/*-------------------------------------------------------------------------
 *
 * selfuncs.c
 *	  标准运算符和索引访问方法的选择性函数和索引成本估算函数。
 *
 *	  选择性例程在 pg_operator 目录中注册
 *	  在 "oprrest" 和 "oprjoin" 属性中。
 *
 *	  索引成本函数通过索引 AM 的 API 结构定位，
 *	  该结构由注册在 pg_am 中的处理函数获取。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/utils/adt/selfuncs.c
 *
 *-------------------------------------------------------------------------
 */

/*----------
 * 操作符选择性估计函数被调用以估计其最上层操作符为其操作符的 WHERE 子句的选择性。
 * 我们将问题分为两种情况：
 *		限制性子句估计：子句仅涉及一个关系的变量。
 *		连接子句估计：子句涉及多个关系的变量。
 * 连接选择性估计比限制估计要困难得多，通常也不太准确。
 *
 * 在处理嵌套循环连接的内部扫描时，我们将连接的连接子句视为内部关系的限制子句，
 * 并将外部关系的变量视为参数（也称为未知值的常量）。因此，限制估计器需要能够接受一个参数
 * 告诉我们哪个关系被视为变量。
 *
 * 限制估计器（oprrest 函数）的调用约定是
 *
 *		Selectivity oprrest (PlannerInfo *root,
 *							 Oid operator,
 *							 List *args,
 *							 int varRelid);
 *
 * root: 关于查询的一般信息（rtable 和 RelOptInfo 列表对估计器特别重要）。
 * operator: 相关操作符的 OID。
 * args: 操作符子句中的参数列表。
 * varRelid: 如果不为零，则表示要视为变量关系的关系的 relid（rtable 索引）。
 * 如果参数列表已知仅包含一个关系的变量，则可以为零。
 *
 * 这在 SQL 层面（在 pg_proc 中）表示为
 *
 *		float8 oprrest (internal, oid, internal, int4);
 *
 * 结果是选择性，即预计会产生 TRUE 结果的关系的行的比例（0 到 1）。
 *
 * 连接估计器（oprjoin 函数）的调用约定类似，
 * 只是 varRelid 不需要，而是提供连接信息：
 *
 *		Selectivity oprjoin (PlannerInfo *root,
 *							 Oid operator,
 *							 List *args,
 *							 JoinType jointype,
 *							 SpecialJoinInfo *sjinfo);
 *
 *		float8 oprjoin (internal, oid, internal, int2, internal);
 *
 * （在 Postgres 8.4 之前，连接估计器只有这四个参数。
 * 该签名仍然允许，但已被弃用。）
 * jointype 与 sjinfo 之间的关系在 clause_selectivity() 的注释中说明——
 * 简而言之，通常最好忽略 jointype，而是检查 sjinfo。
 *
 * 对于常规内部连接和外部连接，连接选择性定义为预期产生 
 * 给定操作符的 TRUE 结果的关系的笛卡尔积的比例（0 到 1）。
 * 然而，对于半连接和反连接，选择性定义为预计在右侧
 * 具有匹配项的左侧关系的行的比例（即，至少有一行具有 TRUE 结果）。
 *
 * 对于 oprrest 和 oprjoin 函数，操作符的输入排序 OID
 *（如果有）使用标准 fmgr 机制传递，以便估计器函数可以通过 PG_GET_COLLATION() 获取。
 * 但是，请注意，pg_statistic 中的所有统计信息目前都是基于相关列的排序构建的。
 *----------
 */

#include "postgres.h"

#include <ctype.h>
#include <math.h>

#include "access/brin.h"
#include "access/brin_page.h"
#include "access/gin.h"
#include "access/table.h"
#include "access/tableam.h"
#include "access/visibilitymap.h"
#include "catalog/pg_am.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_statistic_ext.h"
#include "executor/nodeAgg.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/plancat.h"
#include "parser/parse_clause.h"
#include "parser/parsetree.h"
#include "statistics/statistics.h"
#include "storage/bufmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/date.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
#include "utils/index_selfuncs.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/selfuncs.h"
#include "utils/snapmgr.h"
#include "utils/spccache.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/typcache.h"


/* 插件的钩子，在我们请求统计信息时获取控制权 */
get_relation_stats_hook_type get_relation_stats_hook = NULL;
get_index_stats_hook_type get_index_stats_hook = NULL;

static double fc_eqsel_internal(PG_FUNCTION_ARGS, bool fc_negate);
static double fc_eqjoinsel_inner(Oid fc_opfuncoid, Oid fc_collation,
							  VariableStatData *fc_vardata1, VariableStatData *fc_vardata2,
							  double fc_nd1, double fc_nd2,
							  bool fc_isdefault1, bool fc_isdefault2,
							  AttStatsSlot *fc_sslot1, AttStatsSlot *fc_sslot2,
							  Form_pg_statistic fc_stats1, Form_pg_statistic fc_stats2,
							  bool fc_have_mcvs1, bool fc_have_mcvs2);
static double fc_eqjoinsel_semi(Oid fc_opfuncoid, Oid fc_collation,
							 VariableStatData *fc_vardata1, VariableStatData *fc_vardata2,
							 double fc_nd1, double fc_nd2,
							 bool fc_isdefault1, bool fc_isdefault2,
							 AttStatsSlot *fc_sslot1, AttStatsSlot *fc_sslot2,
							 Form_pg_statistic fc_stats1, Form_pg_statistic fc_stats2,
							 bool fc_have_mcvs1, bool fc_have_mcvs2,
							 RelOptInfo *fc_inner_rel);
static bool fc_estimate_multivariate_ndistinct(PlannerInfo *fc_root,
											RelOptInfo *fc_rel, List **fc_varinfos, double *fc_ndistinct);
static bool fc_convert_to_scalar(Datum fc_value, Oid fc_valuetypid, Oid fc_collid,
							  double *fc_scaledvalue,
							  Datum fc_lobound, Datum fc_hibound, Oid fc_boundstypid,
							  double *fc_scaledlobound, double *fc_scaledhibound);
static double fc_convert_numeric_to_scalar(Datum fc_value, Oid fc_typid, bool *fc_failure);
static void fc_convert_string_to_scalar(char *fc_value,
									 double *fc_scaledvalue,
									 char *fc_lobound,
									 double *fc_scaledlobound,
									 char *fc_hibound,
									 double *fc_scaledhibound);
static void fc_convert_bytea_to_scalar(Datum fc_value,
									double *fc_scaledvalue,
									Datum fc_lobound,
									double *fc_scaledlobound,
									Datum fc_hibound,
									double *fc_scaledhibound);
static double fc_convert_one_string_to_scalar(char *fc_value,
										   int fc_rangelo, int fc_rangehi);
static double fc_convert_one_bytea_to_scalar(unsigned char *fc_value, int fc_valuelen,
										  int fc_rangelo, int fc_rangehi);
static char *fc_convert_string_datum(Datum fc_value, Oid fc_typid, Oid fc_collid,
								  bool *fc_failure);
static double fc_convert_timevalue_to_scalar(Datum fc_value, Oid fc_typid,
										  bool *fc_failure);
static void fc_examine_simple_variable(PlannerInfo *fc_root, Var *fc_var,
									VariableStatData *fc_vardata);
static bool get_variable_range(PlannerInfo *fc_root, VariableStatData *fc_vardata,
							   Oid fc_sortop, Oid fc_collation,
							   Datum *fc_min, Datum *fc_max);
static void get_stats_slot_range(AttStatsSlot *fc_sslot,
								 Oid fc_opfuncoid, FmgrInfo *fc_opproc,
								 Oid fc_collation, int16 fc_typLen, bool fc_typByVal,
								 Datum *fc_min, Datum *fc_max, bool *fc_p_have_data);
static bool get_actual_variable_range(PlannerInfo *fc_root,
									  VariableStatData *fc_vardata,
									  Oid fc_sortop, Oid fc_collation,
									  Datum *fc_min, Datum *fc_max);
static bool get_actual_variable_endpoint(Relation fc_heapRel,
										 Relation fc_indexRel,
										 ScanDirection fc_indexscandir,
										 ScanKey fc_scankeys,
										 int16 fc_typLen,
										 bool fc_typByVal,
										 TupleTableSlot *fc_tableslot,
										 MemoryContext fc_outercontext,
										 Datum *fc_endpointDatum);
static RelOptInfo *fc_find_join_input_rel(PlannerInfo *fc_root, Relids fc_relids);


/*
 *		eqsel			- 任意数据类型的 "=" 的选择性。
 *
 * 注意：此例程还用于估计一些不是 "=" 的操作符的选择性，但具有可比较的选择性行为，
 * 例如 "~="（几何近似匹配）。即使对于 "="，我们也必须记住左右数据类型可能不同。
 */
Datum eqsel(PG_FUNCTION_ARGS)
{
	PG_RETURN_FLOAT8((float8) fc_eqsel_internal(fcinfo, false));
}

/*
 * eqsel() 和 neqsel() 的通用代码
 */
static double fc_eqsel_internal(PG_FUNCTION_ARGS, bool fc_negate)
{
	PlannerInfo *fc_root = (PlannerInfo *) PG_GETARG_POINTER(0);
	Oid			fc_operator = PG_GETARG_OID(1);
	List	   *fc_args = (List *) PG_GETARG_POINTER(2);
	int			fc_varRelid = PG_GETARG_INT32(3);
	Oid			fc_collation = PG_GET_COLLATION();
	VariableStatData fc_vardata;
	Node	   *fc_other;
	bool		fc_varonleft;
	double		fc_selec;

	/*
	 * 当询问关于 <> 时，我们通过相关的 =
	 * 操作符进行估计，然后通过 "1.0 - eq_selectivity - nullfrac" 转换为 <>。
	 */
	if (fc_negate)
	{
		fc_operator = get_negator(fc_operator);
		if (!OidIsValid(fc_operator))
		{
			/* 使用默认选择性（我们应该提高错误吗？） */
			return 1.0 - DEFAULT_EQ_SEL;
		}
	}

	/*
	 * 如果表达式不是变量 = 某个值或某个值 = 变量，则
	 * 直接返回默认估计。
	 */
	if (!get_restriction_variable(fc_root, fc_args, fc_varRelid,
								  &fc_vardata, &fc_other, &fc_varonleft))
		return fc_negate ? (1.0 - DEFAULT_EQ_SEL) : DEFAULT_EQ_SEL;

	/*
	 * 如果某个值是常量，我们可以做得更好。 （注意：Const 可能是估计的结果，而不是查询中的简单常量。）
	 */
	if (IsA(fc_other, Const))
		fc_selec = var_eq_const(&fc_vardata, fc_operator, fc_collation,
							 ((Const *) fc_other)->constvalue,
							 ((Const *) fc_other)->constisnull,
							 fc_varonleft, fc_negate);
	else
		fc_selec = var_eq_non_const(&fc_vardata, fc_operator, fc_collation, fc_other,
								 fc_varonleft, fc_negate);

	ReleaseVariableStats(fc_vardata);

	return fc_selec;
}

/*
 * var_eq_const --- var = const 情况的 eqsel
 *
 * 这个接口已导出，以便其他一些估计函数可以使用它。
 */
double var_eq_const(VariableStatData *fc_vardata, Oid fc_operator, Oid fc_collation,
			 Datum fc_constval, bool fc_constisnull,
			 bool fc_varonleft, bool fc_negate)
{
	double		fc_selec;
	double		fc_nullfrac = 0.0;
	bool		fc_isdefault;
	Oid			fc_opfuncoid;

	/*
	 * 如果常量为 NULL，则假设操作符是严格的并返回零，即，
	 * 操作符将永远不会返回 TRUE。 （即使对于否定操作符也是零。）
	 */
	if (fc_constisnull)
		return 0.0;

	/*
	 * 获取 nullfrac 以供后续使用。注意，我们允许使用 nullfrac
	 * 而不考虑安全检查。
	 */
	if (HeapTupleIsValid(fc_vardata->statsTuple))
	{
		Form_pg_statistic fc_stats;

		fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata->statsTuple);
		fc_nullfrac = fc_stats->stanullfrac;
	}

	/*
	 * 如果我们将变量匹配到一个唯一索引或 DISTINCT 子句，则假设
	 * 无论其他情况如何，恰好有一个匹配。 （这是有点虚假的，因为索引或子句的相等操作符可能与我们的不同，
	 * 但这比忽略该信息更可能是正确的。）
	 */
	if (fc_vardata->isunique && fc_vardata->rel && fc_vardata->rel->tuples >= 1.0)
	{
		fc_selec = 1.0 / fc_vardata->rel->tuples;
	}
	else if (HeapTupleIsValid(fc_vardata->statsTuple) &&
			 statistic_proc_security_check(fc_vardata,
										   (fc_opfuncoid = get_opcode(fc_operator))))
	{
		AttStatsSlot fc_sslot;
		bool		fc_match = false;
		int			fc_i;

		
/*
		 * 常量 "=" 是否与任何列的最常见值相等？
		 * （尽管给定的运算符可能不是 "=", 我们将假设
		 * 检查它是否返回 TRUE 是一种合适的测试。如果你
		 * 不喜欢这样，或许你不该将 eqsel 用于你的
		 * 运算符……）
		 */
		if (get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
							 STATISTIC_KIND_MCV, InvalidOid,
							 ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS))
		{
			LOCAL_FCINFO(fcinfo, 2);
			FmgrInfo	fc_eqproc;

			fmgr_info(fc_opfuncoid, &fc_eqproc);

			/*
			 * 通过只设置一次 fcinfo 结构来节省一些周期。
			 * 直接使用 FunctionCallInvoke 也避免了如果
			 * eqproc 返回 NULL 时的失败，尽管事实上
			 * 相等函数不应该这样做。
			 */
			InitFunctionCallInfoData(*fcinfo, &fc_eqproc, 2, fc_collation,
									 NULL, NULL);
			fcinfo->args[0].isnull = false;
			fcinfo->args[1].isnull = false;
			/* 注意以正确的方式应用运算符 */
			if (fc_varonleft)
				fcinfo->args[1].value = fc_constval;
			else
				fcinfo->args[0].value = fc_constval;

			for (fc_i = 0; fc_i < fc_sslot.nvalues; fc_i++)
			{
				Datum		fc_fresult;

				if (fc_varonleft)
					fcinfo->args[0].value = fc_sslot.values[fc_i];
				else
					fcinfo->args[1].value = fc_sslot.values[fc_i];
				fcinfo->isnull = false;
				fc_fresult = FunctionCallInvoke(fcinfo);
				if (!fcinfo->isnull && DatumGetBool(fc_fresult))
				{
					fc_match = true;
					break;
				}
			}
		}
		else
		{
			/* 没有可用的最常见值信息 */
			fc_i = 0;				/* 保持编译器安静 */
		}

		if (fc_match)
		{
			/*
			 * 常量与这个常见值相等。我们确切地知道选择性
			 * （或至少像 ANALYZE 能计算出的那样准确）。
			 */
			fc_selec = fc_sslot.numbers[fc_i];
		}
		else
		{
			/*
			 * 比较是针对一个既不是 NULL 也不是任何
			 * 常见值的常量。它的选择性不能超过：
			 */
			double		fc_sumcommon = 0.0;
			double		fc_otherdistinct;

			for (fc_i = 0; fc_i < fc_sslot.nnumbers; fc_i++)
				fc_sumcommon += fc_sslot.numbers[fc_i];
			fc_selec = 1.0 - fc_sumcommon - fc_nullfrac;
			CLAMP_PROBABILITY(fc_selec);

			/*
			 * 实际上，它可能要少得多。我们假设所有不常见值
			 * 平均分享剩余的这一部分，因此我们将其除以其他
			 * 不同值的数量。
			 */
			fc_otherdistinct = get_variable_numdistinct(fc_vardata, &fc_isdefault) -
				fc_sslot.nnumbers;
			if (fc_otherdistinct > 1)
				fc_selec /= fc_otherdistinct;

			/*
			 * 另一个交叉检查：选择性不应被估计为超过
			 * 最少的“最常见值”。
			 */
			if (fc_sslot.nnumbers > 0 && fc_selec > fc_sslot.numbers[fc_sslot.nnumbers - 1])
				fc_selec = fc_sslot.numbers[fc_sslot.nnumbers - 1];
		}

		free_attstatsslot(&fc_sslot);
	}
	else
	{
		/*
		 * 没有可用的 ANALYZE 统计信息，因此利用估计的不同值
		 * 数量进行猜测，并假设它们同样常见。（这个猜测
		 * 不太可能非常准确，但我们知道一些特殊情况。）
		 */
		fc_selec = 1.0 / get_variable_numdistinct(fc_vardata, &fc_isdefault);
	}

	/* 现在调整，如果我们想要 <> 而不是 = */
	if (fc_negate)
		fc_selec = 1.0 - fc_selec - fc_nullfrac;

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_selec);

	return fc_selec;
}

/*
 * var_eq_non_const --- eqsel 用于 var = 非常量的情况
 *
 * 这被导出，以便其他一些估计函数可以使用它。
 */
double var_eq_non_const(VariableStatData *fc_vardata, Oid fc_operator, Oid fc_collation,
				 Node *fc_other,
				 bool fc_varonleft, bool fc_negate)
{
	double		fc_selec;
	double		fc_nullfrac = 0.0;
	bool		fc_isdefault;

	/*
	 * 获取 nullfrac 以供下面使用。
	 */
	if (HeapTupleIsValid(fc_vardata->statsTuple))
	{
		Form_pg_statistic fc_stats;

		fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata->statsTuple);
		fc_nullfrac = fc_stats->stanullfrac;
	}

	/*
	 * 如果我们将变量匹配到一个唯一索引或 DISTINCT 子句，则假设
	 * 无论其他情况如何，恰好有一个匹配。 （这是有点虚假的，因为索引或子句的相等操作符可能与我们的不同，
	 * 但这比忽略该信息更可能是正确的。）
	 */
	if (fc_vardata->isunique && fc_vardata->rel && fc_vardata->rel->tuples >= 1.0)
	{
		fc_selec = 1.0 / fc_vardata->rel->tuples;
	}
	else if (HeapTupleIsValid(fc_vardata->statsTuple))
	{
		double		fc_ndistinct;
		AttStatsSlot fc_sslot;

		/*
		 * 搜索是针对一个我们事先不知道的值，但我们将
		 * 假设它不是 NULL。估计选择性为非 NULL 比例除以
		 * 不同值的数量，这样我们就能得到一个覆盖所有可能值
		 * （无论是常见还是不常见）的平均结果。（从本质上
		 * 来说，我们假设尚未知道的比较值有同样的可能性
		 * 是任何可能值，而不管它们在表中的频率。这是个好
		 * 主意吗？）
		 */
		fc_selec = 1.0 - fc_nullfrac;
		fc_ndistinct = get_variable_numdistinct(fc_vardata, &fc_isdefault);
		if (fc_ndistinct > 1)
			fc_selec /= fc_ndistinct;

		/*
		 * 交叉检查：选择性绝不应被估计为超过
		 * 最常见值的。
		 */
		if (get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
							 STATISTIC_KIND_MCV, InvalidOid,
							 ATTSTATSSLOT_NUMBERS))
		{
			if (fc_sslot.nnumbers > 0 && fc_selec > fc_sslot.numbers[0])
				fc_selec = fc_sslot.numbers[0];
			free_attstatsslot(&fc_sslot);
		}
	}
	else
	{
		/*
		 * 没有可用的 ANALYZE 统计信息，因此利用估计的不同值
		 * 数量进行猜测，并假设它们同样常见。（这个猜测
		 * 不太可能非常准确，但我们知道一些特殊情况。）
		 */
		fc_selec = 1.0 / get_variable_numdistinct(fc_vardata, &fc_isdefault);
	}

	/* 现在调整，如果我们想要 <> 而不是 = */
	if (fc_negate)
		fc_selec = 1.0 - fc_selec - fc_nullfrac;

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_selec);

	return fc_selec;
}

/*
 *		neqsel			- 任何数据类型的 "!=" 的选择性。
 *
 * 此例程也用于一些不是 "!=" 的运算符
 * 但具有可比较的选择性行为。请参见上述
 * eqsel() 的注释。
 */
Datum neqsel(PG_FUNCTION_ARGS)
{
	PG_RETURN_FLOAT8((float8) fc_eqsel_internal(fcinfo, true));
}

/*
 *	scalarineqsel		- 比较 "<", "<=", ">", ">=" 对于标量的选择性。
 *
 * 这是 scalarltsel/scalarlesel/scalargtsel/scalargesel 的核心。
 * isgt 和 iseq 标志区分适用哪四种情况。
 *
 * 调用者在必要时已经调换了子句，因此我们可以将
 * 变量视为在左侧。调用者还必须确保子句的另一端
 * 是非 NULL 的常量，并将其剖析为一个值和数据类型。
 * （此定义简化了一些希望以计算值而不是常量节点
 * 进行估计的调用者。）
 *
 * 此例程适用于任何已知的可转换为 scalar() 的数据类型
 * （或数据类型对）。如果它应用于其他数据类型，
 * 将根据假设常量值落在二分查找识别的区间中返回
 * 近似估计。
 */
static double fc_scalarineqsel(PlannerInfo *fc_root, Oid fc_operator, bool fc_isgt, bool fc_iseq,
			  Oid fc_collation,
			  VariableStatData *fc_vardata, Datum fc_constval, Oid fc_consttype)
{
	Form_pg_statistic fc_stats;
	FmgrInfo	fc_opproc;
	double		fc_mcv_selec,
				fc_hist_selec,
				fc_sumcommon;
	double		fc_selec;

	if (!HeapTupleIsValid(fc_vardata->statsTuple))
	{
		/*
		 * 没有可用的统计信息。通常这意味着我们必须依赖
		 * 默认估计；但是如果变量是 CTID，那么我们可以
		 * 根据将常量与表大小进行比较来做出估计。
		 */
		if (fc_vardata->var && IsA(fc_vardata->var, Var) &&
			((Var *) fc_vardata->var)->varattno == SelfItemPointerAttributeNumber)
		{
			ItemPointer fc_itemptr;
			double		fc_block;
			double		fc_density;

			/*
			 * 如果关系为空，我们将包括全部。
			 * （这主要是为了避免下面的除以零。）
			 */
			if (fc_vardata->rel->pages == 0)
				return 1.0;

			fc_itemptr = (ItemPointer) DatumGetPointer(fc_constval);
			fc_block = ItemPointerGetBlockNumberNoCheck(fc_itemptr);

			/*
			 * 确定每页的平均元组数（密度）。
			 *
			 * 由于最后一页平均只有一半的填充，我们可以
			 * 估计它的元组数只有早期页面的一半。因此
			 * 它的权重为常规页面的一半。
			 */
			fc_density = fc_vardata->rel->tuples / (fc_vardata->rel->pages - 0.5);

			/* 如果目标是最后一页，使用一半的密度。 */
			if (fc_block >= fc_vardata->rel->pages - 1)
				fc_density *= 0.5;

			/*
			 * 使用每页的平均元组数，计算 itemptr 可能位于页面
			 * 的多远，并相应地调整块，通过添加整个块的一部分
			 * （但无论 itemptr 的偏移有多高，都不会超过
			 * 整个块）。在这里，我们忽略了死元组行指针的可能性，
			 * 这相当虚假，但我们缺乏更好的信息。
			 */
			if (fc_density > 0.0)
			{
				OffsetNumber fc_offset = ItemPointerGetOffsetNumberNoCheck(fc_itemptr);

				fc_block += Min(fc_offset / fc_density, 1.0);
			}

			/*
			 * 将相对块号转换为选择性。再次强调，最后一页只有一半的权重。
			 */
			fc_selec = fc_block / (fc_vardata->rel->pages - 0.5);

			/*
			 * 到目前为止的计算给出了“<=”情况的选择性。
			 * 对于“<”，我们会少一个元组，对于“>=”，我们会多一个元组，
			 * 后者的选择性将在下面反转，因此我们可以简单地对两种情况减去一个元组。
			 * 需要此调整的情况可以通过iseq等于isgt来识别。
			 */
			if (fc_iseq == fc_isgt && fc_vardata->rel->tuples >= 1.0)
				fc_selec -= (1.0 / fc_vardata->rel->tuples);

			/* 最后，反转“>”、“>=”情况的选择性。 */
			if (fc_isgt)
				fc_selec = 1.0 - fc_selec;

			CLAMP_PROBABILITY(fc_selec);
			return fc_selec;
		}

		/* 没有可用统计信息，因此默认结果 */
		return DEFAULT_INEQ_SEL;
	}
	fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata->statsTuple);

	fmgr_info(get_opcode(fc_operator), &fc_opproc);

	/*
	 * 如果我们有最常见值信息，请将满足 MCV OP CONST
	 * 的 MCV 条目的分数相加。这些分数直接
	 * 影响结果的选择性。还要相加由 MCV 条目表示的
	 * 总分数。
	 */
	fc_mcv_selec = mcv_selectivity(fc_vardata, &fc_opproc, fc_collation, fc_constval, true,
								&fc_sumcommon);

	/*
	 * 如果有直方图，确定常数落入哪个区间，并
	 * 计算对选择性的贡献。
	 */
	fc_hist_selec = ineq_histogram_selectivity(fc_root, fc_vardata,
											fc_operator, &fc_opproc, fc_isgt, fc_iseq,
											fc_collation,
											fc_constval, fc_consttype);

	/*
	 * 现在合并来自MCV和直方图计算的结果，
	 * 意识到直方图仅涵盖未在MCV中列出的非空值。
	 */
	fc_selec = 1.0 - fc_stats->stanullfrac - fc_sumcommon;

	if (fc_hist_selec >= 0.0)
		fc_selec *= fc_hist_selec;
	else
	{
		/*
		 * 如果没有直方图但有未被MCV计算的值，
		 * 可以任意假设其中一半会匹配。
		 */
		fc_selec *= 0.5;
	}

	fc_selec += fc_mcv_selec;

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_selec);

	return fc_selec;
}

/*
 *	mcv_selectivity			- 检查MCV列表以获取选择性估计
 *
 * 确定满足谓词(VAR OP CONST)或(CONST OP VAR)（如果！varonleft）的变量MCV群体的比例。
 * 还计算MCV列表所代表的总列人口的比例。
 * 这段代码对任何返回布尔值的谓词运算符都有效。
 *
 * 函数结果是MCV选择性，返回值的总人口比例存入*sumcommonp。
 * 如果没有MCV列表，则返回零。
 */
double mcv_selectivity(VariableStatData *fc_vardata, FmgrInfo *fc_opproc, Oid fc_collation,
				Datum fc_constval, bool fc_varonleft,
				double *fc_sumcommonp)
{
	double		fc_mcv_selec,
				fc_sumcommon;
	AttStatsSlot fc_sslot;
	int			fc_i;

	fc_mcv_selec = 0.0;
	fc_sumcommon = 0.0;

	if (HeapTupleIsValid(fc_vardata->statsTuple) &&
		statistic_proc_security_check(fc_vardata, fc_opproc->fn_oid) &&
		get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
						 STATISTIC_KIND_MCV, InvalidOid,
						 ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS))
	{
		LOCAL_FCINFO(fcinfo, 2);

		/*
		 * 我们“手动”调用opproc，以免在NULL
		 * 结果上失败。这样的情况在正常比较函数中不会出现，
		 * 但generic_restriction_selectivity可能用于可能返回NULL的运算符。
		 * 一个小的副作用是不需要每次都从头重新初始化fcinfo结构。
		 */
		InitFunctionCallInfoData(*fcinfo, fc_opproc, 2, fc_collation,
								 NULL, NULL);
		fcinfo->args[0].isnull = false;
		fcinfo->args[1].isnull = false;
		/* 注意以正确的方式应用运算符 */
		if (fc_varonleft)
			fcinfo->args[1].value = fc_constval;
		else
			fcinfo->args[0].value = fc_constval;

		for (fc_i = 0; fc_i < fc_sslot.nvalues; fc_i++)
		{
			Datum		fc_fresult;

			if (fc_varonleft)
				fcinfo->args[0].value = fc_sslot.values[fc_i];
			else
				fcinfo->args[1].value = fc_sslot.values[fc_i];
			fcinfo->isnull = false;
			fc_fresult = FunctionCallInvoke(fcinfo);
			if (!fcinfo->isnull && DatumGetBool(fc_fresult))
				fc_mcv_selec += fc_sslot.numbers[fc_i];
			fc_sumcommon += fc_sslot.numbers[fc_i];
		}
		free_attstatsslot(&fc_sslot);
	}

	*fc_sumcommonp = fc_sumcommon;
	return fc_mcv_selec;
}

/*
 *	histogram_selectivity	- 检查直方图以获取选择性估计
 *
 * 确定满足谓词(VAR OP CONST)或(CONST OP VAR)（如果！varonleft）的变量的直方图条目的比例。
 *
 * 这段代码对任何返回布尔值的谓词运算符有效，无论它是否与直方图排序运算符有关。
 * 我们基本上将直方图用作代表性样本。然而，
 * 小直方图不太可能具有很好的代表性，因此调用者应该准备在直方图缺失或非常小时回退到其他估计方法。
 * 当直方图小的时候，将这种方法与另一种方法结合起来可能也是明智的。
 *
 * 如果实际的直方图大小不少于min_hist_size，我们就不必进行计算。
 * 此外，如果n_skip参数> 0，我们将忽略第一个和最后一个n_skip直方图元素，因为
 * 它们是离群值，因此不是很具代表性。这些参数的典型值为10和1。
 *
 * 函数结果是选择性，如果没有直方图或小于min_hist_size则返回-1。
 *
 * 输出参数*hist_size接收实际的直方图大小，
 * 如果没有直方图则为零。调用者可以使用此数字来决定对函数结果的信任程度。
 *
 * 注意结果不考虑最常见值（如果有的话）和空条目。
 * 预计调用者将此结果与列人口的统计信息结合起来。 
 * 在结果范围内限制值，例如，不相信确切的0或1输出，也可能是明智的。
 */
double histogram_selectivity(VariableStatData *fc_vardata,
					  FmgrInfo *fc_opproc, Oid fc_collation,
					  Datum fc_constval, bool fc_varonleft,
					  int fc_min_hist_size, int fc_n_skip,
					  int *fc_hist_size)
{
	double		fc_result;
	AttStatsSlot fc_sslot;

	/* 检查参数的有效性 */
	Assert(fc_n_skip >= 0);
	Assert(fc_min_hist_size > 2 * fc_n_skip);

	if (HeapTupleIsValid(fc_vardata->statsTuple) &&
		statistic_proc_security_check(fc_vardata, fc_opproc->fn_oid) &&
		get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
						 STATISTIC_KIND_HISTOGRAM, InvalidOid,
						 ATTSTATSSLOT_VALUES))
	{
		*fc_hist_size = fc_sslot.nvalues;
		if (fc_sslot.nvalues >= fc_min_hist_size)
		{
			LOCAL_FCINFO(fcinfo, 2);
			int			fc_nmatch = 0;
			int			fc_i;

			/*
			 * 我们“手动”调用opproc，以免在NULL
			 * 结果上失败。这样的情况在正常比较
			 * 函数中不会出现，但generic_restriction_selectivity可能与
			 * 可能返回NULL的运算符一起使用。一个小的副作用是
			 * 不需要每次都重新初始化fcinfo结构。
			 */
			InitFunctionCallInfoData(*fcinfo, fc_opproc, 2, fc_collation,
									 NULL, NULL);
			fcinfo->args[0].isnull = false;
			fcinfo->args[1].isnull = false;
			/* 注意以正确的方式应用运算符 */
			if (fc_varonleft)
				fcinfo->args[1].value = fc_constval;
			else
				fcinfo->args[0].value = fc_constval;

			for (fc_i = fc_n_skip; fc_i < fc_sslot.nvalues - fc_n_skip; fc_i++)
			{
				Datum		fc_fresult;

				if (fc_varonleft)
					fcinfo->args[0].value = fc_sslot.values[fc_i];
				else
					fcinfo->args[1].value = fc_sslot.values[fc_i];
				fcinfo->isnull = false;
				fc_fresult = FunctionCallInvoke(fcinfo);
				if (!fcinfo->isnull && DatumGetBool(fc_fresult))
					fc_nmatch++;
			}
			fc_result = ((double) fc_nmatch) / ((double) (fc_sslot.nvalues - 2 * fc_n_skip));
		}
		else
			fc_result = -1;
		free_attstatsslot(&fc_sslot);
	}
	else
	{
		*fc_hist_size = 0;
		fc_result = -1;
	}

	return fc_result;
}


/*
 *	generic_restriction_selectivity		- 对几乎任何事物的选择性
 *
 * 此函数估计我们没有任何特别知识但在我们收集标准
 * MCV 和/或直方图统计的数据类型上的操作符的选择性。  (额外的假设是
 * 操作符是严格的和不可变的，或者至少是稳定的。)
 *
 * 如果我们有 "VAR OP CONST" 或 "CONST OP VAR"，选择性通过
 * 将操作符应用于列的 MCV 和/或直方图统计的每个元素来估计，
 * 并使用假设直方图是列的非 MCV 人口的合理随机样本来合并结果。 请注意，
 * 如果操作符的语义与直方图排序相关，这可能不是一个很好的假设；其他函数如
 * scalarineqsel() 在这种情况下可能更合适。
 *
 * 否则，退回调用者提供的默认选择性。
 */
double generic_restriction_selectivity(PlannerInfo *fc_root, Oid fc_oproid, Oid fc_collation,
								List *fc_args, int fc_varRelid,
								double fc_default_selectivity)
{
	double		fc_selec;
	VariableStatData fc_vardata;
	Node	   *fc_other;
	bool		fc_varonleft;

	/*
	 * 如果表达式不是变量 OP 某物或某物 OP 变量，
	 * 那么就返回默认估计。
	 */
	if (!get_restriction_variable(fc_root, fc_args, fc_varRelid,
								  &fc_vardata, &fc_other, &fc_varonleft))
		return fc_default_selectivity;

	/*
	 * 如果某物是 NULL 常量，则假设操作符是严格的并
	 * 返回零，即操作符永远不会返回 TRUE。
	 */
	if (IsA(fc_other, Const) &&
		((Const *) fc_other)->constisnull)
	{
		ReleaseVariableStats(fc_vardata);
		return 0.0;
	}

	if (IsA(fc_other, Const))
	{
		/* 变量正在与已知的非空常量进行比较 */
		Datum		fc_constval = ((Const *) fc_other)->constvalue;
		FmgrInfo	fc_opproc;
		double		fc_mcvsum;
		double		fc_mcvsel;
		double		fc_nullfrac;
		int			fc_hist_size;

		fmgr_info(get_opcode(fc_oproid), &fc_opproc);

		/*
		 * 计算列最常见值的选择性。
		 */
		fc_mcvsel = mcv_selectivity(&fc_vardata, &fc_opproc, fc_collation,
								 fc_constval, fc_varonleft,
								 &fc_mcvsum);

		/*
		 * 如果直方图足够大，查看其中匹配
		 * 查询的部分，并假设这代表了非 MCV
		 * 人口。 否则使用非 MCV
		 * 人口的默认选择性。
		 */
		fc_selec = histogram_selectivity(&fc_vardata, &fc_opproc, fc_collation,
									  fc_constval, fc_varonleft,
									  10, 1, &fc_hist_size);
		if (fc_selec < 0)
		{
			/* 不用，退回到默认值 */
			fc_selec = fc_default_selectivity;
		}
		else if (fc_hist_size < 100)
		{
			/*
			 * 对于从 10 到 100 的直方图大小，我们结合直方图
			 * 和默认选择性，对于更大的尺寸，我们对
			 * 直方图的信任度不断增加。
			 */
			double		fc_hist_weight = fc_hist_size / 100.0;

			fc_selec = fc_selec * fc_hist_weight +
				fc_default_selectivity * (1.0 - fc_hist_weight);
		}

		/* 无论如何，不要相信极小或极大的估计。 */
		if (fc_selec < 0.0001)
			fc_selec = 0.0001;
		else if (fc_selec > 0.9999)
			fc_selec = 0.9999;

		/* 别忘了考虑 NULL。 */
		if (HeapTupleIsValid(fc_vardata.statsTuple))
			fc_nullfrac = ((Form_pg_statistic) GETSTRUCT(fc_vardata.statsTuple))->stanullfrac;
		else
			fc_nullfrac = 0.0;

		/*
		 * 现在合并来自 MCV 和直方图计算的结果， 
		 * 意识到直方图仅涵盖未列出的非空值。
		 */
		fc_selec *= 1.0 - fc_nullfrac - fc_mcvsum;
		fc_selec += fc_mcvsel;
	}
	else
	{
		/* 比较值不是常量，所以我们无法做任何事情 */
		fc_selec = fc_default_selectivity;
	}

	ReleaseVariableStats(fc_vardata);

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_selec);

	return fc_selec;
}

/*
 *	ineq_histogram_selectivity	- 检查 scalarineqsel 的直方图
 *
 * 确定满足不等式条件的变量直方图人群的比例，即 VAR <（或 <=，>，>=） CONST。
 * isgt 和 iseq 标志区分哪个四个情况适用。
 *
 * 虽然可以通过操作符 OID 查找 opproc，但普通调用者
 * 也需要单独调用它，因此我们让调用者同时传递两个。
 *
 * 如果没有直方图，则返回 -1（有效结果将始终大于或等于 0）。
 *
 * 注意，结果忽略了最常见值（如果有的话）和
 * NULL 条目。 期望调用者将此结果与
 * 列人口的那些部分的统计信息结合起来。
 *
 * 这是导出的，以便其他一些估计函数可以使用它。
 */
double ineq_histogram_selectivity(PlannerInfo *fc_root,
						   VariableStatData *fc_vardata,
						   Oid fc_opoid, FmgrInfo *fc_opproc, bool fc_isgt, bool fc_iseq,
						   Oid fc_collation,
						   Datum fc_constval, Oid fc_consttype)
{
	double		fc_hist_selec;
	AttStatsSlot fc_sslot;

	fc_hist_selec = -1.0;

	/*
	 * 总有一天，ANALYZE 可能会为每个 rel/att 存储多个直方图，
	 * 对应于为列类型定义的多个可能的排序顺序。 现在，我们知道只有一个，
	 * 所以只需获取它并查看它是否与查询匹配。
	 *
	 * 注意，我们不能使用 opoid 作为搜索参数；在
	 * pg_statistic 中出现的 staop 将是相关的 '<' 操作符，但我们拥有的
	 * 可能是某个其他不等式操作符，例如 '>='。(即使 opoid
	 * 是 '<' 操作符，它也可能是跨类型的。)因此，我们必须使用
	 * comparison_ops_are_compatible() 来查看操作符是否匹配。
	 */
	if (HeapTupleIsValid(fc_vardata->statsTuple) &&
		statistic_proc_security_check(fc_vardata, fc_opproc->fn_oid) &&
		get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
						 STATISTIC_KIND_HISTOGRAM, InvalidOid,
						 ATTSTATSSLOT_VALUES))
	{
		if (fc_sslot.nvalues > 1 &&
			fc_sslot.stacoll == fc_collation &&
			comparison_ops_are_compatible(fc_sslot.staop, fc_opoid))
		{
			/*
			 * 使用二分搜索找到所需的位置，即
			 * 包含比较值的直方图箱的右端，
			 * 这是比较操作符成功（如果是 isgt）或失败（如果不是 isgt）
			 * 的左侧条目。
			 *
			 * 在这个循环中，我们不关注操作符是否是
			 * 还是不是 iseq；这个细节将在下面处理。（无论如何，我们无法判断，
			 * 操作符是否认为值是相等的。）
			 *
			 * 如果二分搜索访问到第一个或最后一个直方图
			 * 条目，我们尝试通过 get_actual_variable_range() 找到的
			 * 实际列最小值或最大值来替换该端点。 这
			 * 改善了由于自上次 ANALYZE 以来的更改所造成的最小或最大值移动时的
			 * 错误估计。 请注意，这可能会导致实际上将 MCV 包含到直方图中，
			 * 但我们不尝试对此进行修正。
			 */
			double		fc_histfrac;
			int			fc_lobound = 0;	/* 搜索的第一个可能槽 */
			int			fc_hibound = fc_sslot.nvalues;	/* 搜索的最后一个+1 槽 */
			bool		fc_have_end = false;

			/*
			 * 如果只有两个直方图条目，我们希望两个都具有最新值。
			 * （如果超过两个，我们最多需要其中一个进行更新，所以我们在
			 * 循环中处理这个问题。）
			 */
			if (fc_sslot.nvalues == 2)
				fc_have_end = get_actual_variable_range(fc_root,
													 fc_vardata,
													 fc_sslot.staop,
													 fc_collation,
													 &fc_sslot.values[0],
													 &fc_sslot.values[1]);

			while (fc_lobound < fc_hibound)
			{
				int			fc_probe = (fc_lobound + fc_hibound) / 2;
				bool		fc_ltcmp;

				/*
				 * 如果我们发现自己即将与第一个或最后一个
				 * 直方图条目进行比较，首先尝试用实际的
				 * 当前最小值或最大值替换它（除非我们已经在上面这样做了）。
				 */
				if (fc_probe == 0 && fc_sslot.nvalues > 2)
					fc_have_end = get_actual_variable_range(fc_root,
														 fc_vardata,
														 fc_sslot.staop,
														 fc_collation,
														 &fc_sslot.values[0],
														 NULL);
				else if (fc_probe == fc_sslot.nvalues - 1 && fc_sslot.nvalues > 2)
					fc_have_end = get_actual_variable_range(fc_root,
														 fc_vardata,
														 fc_sslot.staop,
														 fc_collation,
														 NULL,
														 &fc_sslot.values[fc_probe]);

				fc_ltcmp = DatumGetBool(FunctionCall2Coll(fc_opproc,
													   fc_collation,
													   fc_sslot.values[fc_probe],
													   fc_constval));
				if (fc_isgt)
					fc_ltcmp = !fc_ltcmp;
				if (fc_ltcmp)
					fc_lobound = fc_probe + 1;
				else
					fc_hibound = fc_probe;
			}

			if (fc_lobound <= 0)
			{
				
				fc_histfrac = 0.0;
			}
			else if (fc_lobound >= fc_sslot.nvalues)
			{
				/*
				 * 反向情况：常数在上边界的直方图之上。
				 */
				fc_histfrac = 1.0;
			}
			else
			{
				/* 我们有 values[i-1] <= constant <= values[i]. */
				int			fc_i = fc_lobound;
				double		fc_eq_selec = 0;
				double		fc_val,
							fc_high,
							fc_low;
				double		fc_binfrac;

				/*
				 * 在我们需要下面的情况下，获得“x = constval”的选择性估计。
				 * 我们使用类似于 var_eq_const() 对非 MCV 常量所做的计算，
				 * 即估计所有不同的非 MCV 值出现的频率相等。
				 * 但是，乘以“1.0 - sumcommon - nullfrac”
				 * 将由我们的调用者完成，所以我们在这里不应该这样做。
				 * 因此，我们无法尝试通过引用最少常见的 MCV 来限制估计；
				 * 结果会太小。
				 *
				 * 注意：由于这实际上假设 constval 不是 MCV，
				 * 如果 constval 实际上是一个 MCV，这在逻辑上是可疑的。
				 * 但我们必须对等式应用某种校正，
				 * 而且我们无法判断 constval 是否是 MCV，因为我们
				 * 没有合适的等式运算符。
				 */
				if (fc_i == 1 || fc_isgt == fc_iseq)
				{
					double		fc_otherdistinct;
					bool		fc_isdefault;
					AttStatsSlot fc_mcvslot;

					/* 获取估计的不同值的数量 */
					fc_otherdistinct = get_variable_numdistinct(fc_vardata,
															 &fc_isdefault);

					/* 减去已知的 MCV 数量 */
					if (get_attstatsslot(&fc_mcvslot, fc_vardata->statsTuple,
										 STATISTIC_KIND_MCV, InvalidOid,
										 ATTSTATSSLOT_NUMBERS))
					{
						fc_otherdistinct -= fc_mcvslot.nnumbers;
						free_attstatsslot(&fc_mcvslot);
					}

					/* 如果结果看起来不合理，则将 eq_selec 设为 0 */
					if (fc_otherdistinct > 1)
						fc_eq_selec = 1.0 / fc_otherdistinct;
				}

				/*
				 * 将常数和两个最邻近的桶边界值转换为统一的比较尺度，
				 * 并在这个桶内进行线性插值。
				 */
				if (fc_convert_to_scalar(fc_constval, fc_consttype, fc_collation,
									  &fc_val,
									  fc_sslot.values[fc_i - 1], fc_sslot.values[fc_i],
									  fc_vardata->vartype,
									  &fc_low, &fc_high))
				{
					if (fc_high <= fc_low)
					{
						/* 如果桶边界看起来相同，则进行处理 */
						fc_binfrac = 0.5;
					}
					else if (fc_val <= fc_low)
						fc_binfrac = 0.0;
					else if (fc_val >= fc_high)
						fc_binfrac = 1.0;
					else
					{
						fc_binfrac = (fc_val - fc_low) / (fc_high - fc_low);

						/*
						 * 注意我们得到 NaN 或
						 * Infinity 的可能性。这可能发生
						 * 尽管进行了先前的检查，例如“low”
						 * 是 -Infinity 时。
						 */
						if (isnan(fc_binfrac) ||
							fc_binfrac < 0.0 || fc_binfrac > 1.0)
							fc_binfrac = 0.5;
					}
				}
				else
				{
					/*
					 * 理想情况下，我们会在这里产生一个错误，理由是
					 * 给定的运算符不应该将 scalarXXsel
					 * 注册为其选择性函数，除非我们能处理
					 * 其操作数类型。但是目前，各种
					 * 东西正在调用 scalarXXsel，因此给出一个默认
					 * 估计，直到可以修复。
					 */
					fc_binfrac = 0.5;
				}

				/*
				 * 现在，计算通过直方图表示的值的整体选择性。
				 * 我们有 i-1 个完整的桶和小于常数的 binfrac 部分桶。
				 */
				fc_histfrac = (double) (fc_i - 1) + fc_binfrac;
				fc_histfrac /= (double) (fc_sslot.nvalues - 1);

				/*
				 * 此时，histfrac 是表示直方图的
				 * 人口中满足“x <= constval”的部分的估计值。
				 * 相当值得注意的是，这个说法在我们使用的任何运算符下都成立，
				 * 只要 convert_to_scalar() 返回合理的
				 * 结果。如果探测常数等于某个直方图
				 * 条目，我们会考虑到该条目左侧的桶，
				 * 如果用“<”或“>=”进行探测，或者考虑到右侧的桶
				 * 如果用“<=”或“>”进行探测；但是在第一种情况下，binfrac 将
				 * 结果设为 1.0，在第二种情况下设置为 0.0，
				 * 结果在两种情况下都是相同的 histfrac。对于位于直方图条目之间的探测常数，
				 * 我们找到相同的桶，并用任何运算符得到相同的估计。
				 *
				 * 估计对应于“x <= constval”，而不是“x < constval”的事实
				 * 是因为 ANALYZE 构建直方图的方式：每个条目实际上是
				 * 其样本桶中的最右侧值。因此，选择性
				 * 值为 1/(histogram_size-1) 的确切倍数
				 * 应被理解为包括一个直方图
				 * 条目加上其左侧的所有东西的估计。
				 *
				 * 然而，这在第一个直方图条目中失效，
				 * 该条目必然是其样本桶中的最左侧值。
				 * 这意味着第一个直方图桶略窄于其余桶，
				 * 它的宽度等于 eq_selec。
				 * 另一种说法是，我们希望“x <= leftmost”被
				 * 估计为 eq_selec 而不是零。因此，如果我们在处理
				 * 第一个桶（i==1），则重新缩放使其成立，同时
				 * 线性调整该桶的其余部分。
				 */
				if (fc_i == 1)
					fc_histfrac += fc_eq_selec * (1.0 - fc_binfrac);

				/*
				 * 如果我们想要“x <= constval”的估计，用于“<=”或
				 * “>”，但如果我们正在估计“<”或“>=”，我们现在需要
				 * 减少 eq_selec 的估计。
				 */
				if (fc_isgt == fc_iseq)
					fc_histfrac -= fc_eq_selec;
			}

			/*
			 * 现在“<”和“<=”情况下的估计完成了。如果我们在
			 * 估计“>”或“>=”，则翻转它。
			 */
			fc_hist_selec = fc_isgt ? (1.0 - fc_histfrac) : fc_histfrac;

			/*
			 * 直方图边界起初只是近似的，
			 * 并且很可能过时。因此，不要相信
			 * 极小或极大的选择性估计——除非我们
			 * 从表中获得了实际的当前端点值，在这种情况下只需进行通常的合理性调整。
			 * 有些任意性地，我们将其他情况的截止值设为直方图
			 * 分辨率的百分之一。
			 */
			if (fc_have_end)
				CLAMP_PROBABILITY(fc_hist_selec);
			else
			{
				double		fc_cutoff = 0.01 / (double) (fc_sslot.nvalues - 1);

				if (fc_hist_selec < fc_cutoff)
					fc_hist_selec = fc_cutoff;
				else if (fc_hist_selec > 1.0 - fc_cutoff)
					fc_hist_selec = 1.0 - fc_cutoff;
			}
		}
		else if (fc_sslot.nvalues > 1)
		{
			
/*
			 * 如果我们到这里，说明我们有一个直方图，但它的排序方式
			 * 不是我们想要的。进行暴力搜索以查看有多少
			 * 条目满足比较条件，并将该
			 * 比例作为我们的估计。 （这与 histogram_selectivity 的内部循环
			 * 相同；也许可以共享代码？）
			 */
			LOCAL_FCINFO(fcinfo, 2);
			int			fc_nmatch = 0;

			InitFunctionCallInfoData(*fcinfo, fc_opproc, 2, fc_collation,
									 NULL, NULL);
			fcinfo->args[0].isnull = false;
			fcinfo->args[1].isnull = false;
			fcinfo->args[1].value = fc_constval;
			for (int fc_i = 0; fc_i < fc_sslot.nvalues; fc_i++)
			{
				Datum		fc_fresult;

				fcinfo->args[0].value = fc_sslot.values[fc_i];
				fcinfo->isnull = false;
				fc_fresult = FunctionCallInvoke(fcinfo);
				if (!fcinfo->isnull && DatumGetBool(fc_fresult))
					fc_nmatch++;
			}
			fc_hist_selec = ((double) fc_nmatch) / ((double) fc_sslot.nvalues);

			/*
			 * 如上所述，限制为直方图分辨率的百分之一。
			 * 这种情况肯定比正常情况更不可信，
			 * 所以我们不应该相信确切的 0 或 1 选择性。 （在这种情况下，限制条件可能应该更严格？）
			 */
			{
				double		fc_cutoff = 0.01 / (double) (fc_sslot.nvalues - 1);

				if (fc_hist_selec < fc_cutoff)
					fc_hist_selec = fc_cutoff;
				else if (fc_hist_selec > 1.0 - fc_cutoff)
					fc_hist_selec = 1.0 - fc_cutoff;
			}
		}

		free_attstatsslot(&fc_sslot);
	}

	return fc_hist_selec;
}

/*
 * 选择性估算器的通用包装函数，仅仅
 * 调用 scalarineqsel()。
 */
static Datum fc_scalarineqsel_wrapper(PG_FUNCTION_ARGS, bool fc_isgt, bool fc_iseq)
{
	PlannerInfo *fc_root = (PlannerInfo *) PG_GETARG_POINTER(0);
	Oid			fc_operator = PG_GETARG_OID(1);
	List	   *fc_args = (List *) PG_GETARG_POINTER(2);
	int			fc_varRelid = PG_GETARG_INT32(3);
	Oid			fc_collation = PG_GET_COLLATION();
	VariableStatData fc_vardata;
	Node	   *fc_other;
	bool		fc_varonleft;
	Datum		fc_constval;
	Oid			fc_consttype;
	double		fc_selec;

	/*
	 * 如果表达式不是变量与某个东西的操作或某个东西与变量的操作，
	 * 那么返回默认估计。
	 */
	if (!get_restriction_variable(fc_root, fc_args, fc_varRelid,
								  &fc_vardata, &fc_other, &fc_varonleft))
		PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);

	/*
	 * 如果 something 也不是常量，就无法做任何有用的事情。
	 */
	if (!IsA(fc_other, Const))
	{
		ReleaseVariableStats(fc_vardata);
		PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
	}

	/*
	 * 如果常量为 NULL，假设操作符是严格的，返回零，即，
	 * 操作符将永远不会返回 TRUE。
	 */
	if (((Const *) fc_other)->constisnull)
	{
		ReleaseVariableStats(fc_vardata);
		PG_RETURN_FLOAT8(0.0);
	}
	fc_constval = ((Const *) fc_other)->constvalue;
	fc_consttype = ((Const *) fc_other)->consttype;

	/*
	 * 强制变量在左侧，以简化 scalarineqsel 中的逻辑。
	 */
	if (!fc_varonleft)
	{
		fc_operator = get_commutator(fc_operator);
		if (!fc_operator)
		{
			/* 使用默认选择性（我们应该提高错误吗？） */
			ReleaseVariableStats(fc_vardata);
			PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
		}
		fc_isgt = !fc_isgt;
	}

	/* 剩下的工作由 scalarineqsel() 完成。 */
	fc_selec = fc_scalarineqsel(fc_root, fc_operator, fc_isgt, fc_iseq, fc_collation,
						  &fc_vardata, fc_constval, fc_consttype);

	ReleaseVariableStats(fc_vardata);

	PG_RETURN_FLOAT8((float8) fc_selec);
}

/*
 *		scalarltsel		- 标量的 "<" 选择性。
 */
Datum scalarltsel(PG_FUNCTION_ARGS)
{
	return fc_scalarineqsel_wrapper(fcinfo, false, false);
}

/*
 *		scalarlesel		- 标量的 "<=" 选择性。
 */
Datum scalarlesel(PG_FUNCTION_ARGS)
{
	return fc_scalarineqsel_wrapper(fcinfo, false, true);
}

/*
 *		scalargtsel		- 标量的 ">" 选择性。
 */
Datum scalargtsel(PG_FUNCTION_ARGS)
{
	return fc_scalarineqsel_wrapper(fcinfo, true, false);
}

/*
 *		scalargesel		- 标量的 ">=" 选择性。
 */
Datum scalargesel(PG_FUNCTION_ARGS)
{
	return fc_scalarineqsel_wrapper(fcinfo, true, true);
}

/*
 *		boolvarsel		- 布尔变量的选择性。
 *
 * 实际上可以在任何布尔值表达式上调用它。如果它仅涉及指定关系的 Var，
 * 并且如果有关于 Var 或表达式的统计信息
 * （如果它被索引则可以），那么我们将产生一个真实的估计；否则只能是默认值。
 */
Selectivity boolvarsel(PlannerInfo *fc_root, Node *fc_arg, int fc_varRelid)
{
	VariableStatData fc_vardata;
	double		fc_selec;

	examine_variable(fc_root, fc_arg, fc_varRelid, &fc_vardata);
	if (HeapTupleIsValid(fc_vardata.statsTuple))
	{
		/*
		 * 布尔变量 V 等同于子句 V = 't'，所以我们
		 * 将选择性计算为我们所拥有的那样。
		 */
		fc_selec = var_eq_const(&fc_vardata, BooleanEqualOperator, InvalidOid,
							 BoolGetDatum(true), false, true, false);
	}
	else
	{
		/* 否则，默认估计为 0.5 */
		fc_selec = 0.5;
	}
	ReleaseVariableStats(fc_vardata);
	return fc_selec;
}

/*
 *		booltestsel		- 布尔测试节点的选择性。
 */
Selectivity booltestsel(PlannerInfo *fc_root, BoolTestType fc_booltesttype, Node *fc_arg,
			int fc_varRelid, JoinType fc_jointype, SpecialJoinInfo *fc_sjinfo)
{
	VariableStatData fc_vardata;
	double		fc_selec;

	examine_variable(fc_root, fc_arg, fc_varRelid, &fc_vardata);

	if (HeapTupleIsValid(fc_vardata.statsTuple))
	{
		Form_pg_statistic fc_stats;
		double		fc_freq_null;
		AttStatsSlot fc_sslot;

		fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata.statsTuple);
		fc_freq_null = fc_stats->stanullfrac;

		if (get_attstatsslot(&fc_sslot, fc_vardata.statsTuple,
							 STATISTIC_KIND_MCV, InvalidOid,
							 ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS)
			&& fc_sslot.nnumbers > 0)
		{
			double		fc_freq_true;
			double		fc_freq_false;

			/*
			 * 获取第一个MCV频率并推导真实的频率。
			 */
			if (DatumGetBool(fc_sslot.values[0]))
				fc_freq_true = fc_sslot.numbers[0];
			else
				fc_freq_true = 1.0 - fc_sslot.numbers[0] - fc_freq_null;

			/*
			 * 接下来推导假值的频率。然后根据需要使用这些频率
			 * 来推导每种情况的频率。
			 */
			fc_freq_false = 1.0 - fc_freq_true - fc_freq_null;

			switch (fc_booltesttype)
			{
				case IS_UNKNOWN:
					/* 仅选择NULL值 */
					fc_selec = fc_freq_null;
					break;
				case IS_NOT_UNKNOWN:
					/* 选择非NULL值 */
					fc_selec = 1.0 - fc_freq_null;
					break;
				case IS_TRUE:
					/* 仅选择TRUE值 */
					fc_selec = fc_freq_true;
					break;
				case IS_NOT_TRUE:
					/* 选择非TRUE值 */
					fc_selec = 1.0 - fc_freq_true;
					break;
				case IS_FALSE:
					/* 仅选择FALSE值 */
					fc_selec = fc_freq_false;
					break;
				case IS_NOT_FALSE:
					/* 选择非FALSE值 */
					fc_selec = 1.0 - fc_freq_false;
					break;
				default:
					elog(ERROR, "unrecognized booltesttype: %d",
						 (int) fc_booltesttype);
					fc_selec = 0.0;	/* 让编译器安静 */
					break;
			}

			free_attstatsslot(&fc_sslot);
		}
		else
		{
			/*
			 * 没有可用的最常见值信息。仍然有NULL比例的信息，
			 * 因此将其用于IS [NOT] UNKNOWN。否则调整
			 * NULL比例，并假设TRUE和FALSE的50-50分配。
			 */
			switch (fc_booltesttype)
			{
				case IS_UNKNOWN:
					/* 仅选择NULL值 */
					fc_selec = fc_freq_null;
					break;
				case IS_NOT_UNKNOWN:
					/* 选择非NULL值 */
					fc_selec = 1.0 - fc_freq_null;
					break;
				case IS_TRUE:
				case IS_FALSE:
					/* 假设我们选择一半的非NULL值 */
					fc_selec = (1.0 - fc_freq_null) / 2.0;
					break;
				case IS_NOT_TRUE:
				case IS_NOT_FALSE:
					/* 假设我们选择NULL和一半的非NULL值 */
					/* 相当于freq_null + (1.0 - freq_null) / 2.0 */
					fc_selec = (fc_freq_null + 1.0) / 2.0;
					break;
				default:
					elog(ERROR, "unrecognized booltesttype: %d",
						 (int) fc_booltesttype);
					fc_selec = 0.0;	/* 让编译器安静 */
					break;
			}
		}
	}
	else
	{
		/*
		 * 如果我们无法获取参数的变量统计信息，也许
		 * clause_selectivity可以处理它。我们忽略
		 * 使用clause_selectivity时NULL值的可能性，仅
		 * 假设该值是TRUE或FALSE。
		 */
		switch (fc_booltesttype)
		{
			case IS_UNKNOWN:
				fc_selec = DEFAULT_UNK_SEL;
				break;
			case IS_NOT_UNKNOWN:
				fc_selec = DEFAULT_NOT_UNK_SEL;
				break;
			case IS_TRUE:
			case IS_NOT_FALSE:
				fc_selec = (double) clause_selectivity(fc_root, fc_arg,
													fc_varRelid,
													fc_jointype, fc_sjinfo);
				break;
			case IS_FALSE:
			case IS_NOT_TRUE:
				fc_selec = 1.0 - (double) clause_selectivity(fc_root, fc_arg,
														  fc_varRelid,
														  fc_jointype, fc_sjinfo);
				break;
			default:
				elog(ERROR, "unrecognized booltesttype: %d",
					 (int) fc_booltesttype);
				fc_selec = 0.0;	/* 让编译器安静 */
				break;
		}
	}

	ReleaseVariableStats(fc_vardata);

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_selec);

	return (Selectivity) fc_selec;
}

/*
 *		nulltestsel		- NullTest节点的选择性。
 */
Selectivity nulltestsel(PlannerInfo *fc_root, NullTestType fc_nulltesttype, Node *fc_arg,
			int fc_varRelid, JoinType fc_jointype, SpecialJoinInfo *fc_sjinfo)
{
	VariableStatData fc_vardata;
	double		fc_selec;

	examine_variable(fc_root, fc_arg, fc_varRelid, &fc_vardata);

	if (HeapTupleIsValid(fc_vardata.statsTuple))
	{
		Form_pg_statistic fc_stats;
		double		fc_freq_null;

		fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata.statsTuple);
		fc_freq_null = fc_stats->stanullfrac;

		switch (fc_nulltesttype)
		{
			case IS_NULL:

				/*
				 * 直接使用freq_null。
				 */
				fc_selec = fc_freq_null;
				break;
			case IS_NOT_NULL:

				/*
				 * 选择非未知（非NULL）值。根据
				 * freq_null进行计算。
				 */
				fc_selec = 1.0 - fc_freq_null;
				break;
			default:
				elog(ERROR, "unrecognized nulltesttype: %d",
					 (int) fc_nulltesttype);
				return (Selectivity) 0; /* 保持编译器安静 */
		}
	}
	else if (fc_vardata.var && IsA(fc_vardata.var, Var) &&
			 ((Var *) fc_vardata.var)->varattno < 0)
	{
		/*
		 * 系统列没有统计信息，但我们知道它们永远不是
		 * NULL。
		 */
		fc_selec = (fc_nulltesttype == IS_NULL) ? 0.0 : 1.0;
	}
	else
	{
		/*
		 * 没有可用的ANALYZE统计信息，因此进行猜测
		 */
		switch (fc_nulltesttype)
		{
			case IS_NULL:
				fc_selec = DEFAULT_UNK_SEL;
				break;
			case IS_NOT_NULL:
				fc_selec = DEFAULT_NOT_UNK_SEL;
				break;
			default:
				elog(ERROR, "unrecognized nulltesttype: %d",
					 (int) fc_nulltesttype);
				return (Selectivity) 0; /* 保持编译器安静 */
		}
	}

	ReleaseVariableStats(fc_vardata);

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_selec);

	return (Selectivity) fc_selec;
}

/*
 * strip_array_coercion - 从数组表达式中剥离二进制兼容的重新标记
 *
 * 对于数组值，解析器通常生成ArrayCoerceExpr转换，
 * 但似乎RelabelType也可能出现。此外，规划器
 * 目前对折叠堆叠的ArrayCoerceExpr节点并不紧张，
 * 所以我们需要准备好处理不止一级。
 */
static Node * fc_strip_array_coercion(Node *fc_node)
{
	for (;;)
	{
		if (fc_node && IsA(fc_node, ArrayCoerceExpr))
		{
			ArrayCoerceExpr *fc_acoerce = (ArrayCoerceExpr *) fc_node;

			/*
			 * 如果每个元素的表达式仅仅是CaseTestExpr上方的
			 * RelabelType，那么我们知道这是一个二进制兼容的重新标记。
			 */
			if (IsA(fc_acoerce->elemexpr, RelabelType) &&
				IsA(((RelabelType *) fc_acoerce->elemexpr)->arg, CaseTestExpr))
				fc_node = (Node *) fc_acoerce->arg;
			else
				break;
		}
		else if (fc_node && IsA(fc_node, RelabelType))
		{
			/* 我们并不真正期望这种情况，但可以处理 */
			fc_node = (Node *) ((RelabelType *) fc_node)->arg;
		}
		else
			break;
	}
	return fc_node;
}

/*
 *		scalararraysel		- ScalarArrayOpExpr节点的选择性。
 */
Selectivity scalararraysel(PlannerInfo *fc_root,
			   ScalarArrayOpExpr *fc_clause,
			   bool fc_is_join_clause,
			   int fc_varRelid,
			   JoinType fc_jointype,
			   SpecialJoinInfo *fc_sjinfo)
{
	Oid			fc_operator = fc_clause->opno;
	bool		fc_useOr = fc_clause->useOr;
	bool		fc_isEquality = false;
	bool		fc_isInequality = false;
	Node	   *fc_leftop;
	Node	   *fc_rightop;
	Oid			fc_nominal_element_type;
	Oid			fc_nominal_element_collation;
	TypeCacheEntry *fc_typentry;
	RegProcedure fc_oprsel;
	FmgrInfo	fc_oprselproc;
	Selectivity fc_s1;
	Selectivity fc_s1disjoint;

	/* 首先，解构表达式 */
	Assert(list_length(fc_clause->args) == 2);
	fc_leftop = (Node *) linitial(fc_clause->args);
	fc_rightop = (Node *) lsecond(fc_clause->args);

	/* 积极减少两边到常量 */
	fc_leftop = estimate_expression_value(fc_root, fc_leftop);
	fc_rightop = estimate_expression_value(fc_root, fc_rightop);

	/* 获取右操作数的名义（重新标记后）元素类型 */
	fc_nominal_element_type = get_base_element_type(exprType(fc_rightop));
	if (!OidIsValid(fc_nominal_element_type))
		return (Selectivity) 0.5;	/* 这可能不应该发生 */
	/* 也获取用于生成常量的名义排序 */
	fc_nominal_element_collation = exprCollation(fc_rightop);

	/* 查找右操作数的任何二进制兼容的重新标记 */
	fc_rightop = fc_strip_array_coercion(fc_rightop);

	/*
	 * 检测操作符是否为数组元素类型的默认相等或不相等
	 * 操作符。
	 */
	fc_typentry = lookup_type_cache(fc_nominal_element_type, TYPECACHE_EQ_OPR);
	if (OidIsValid(fc_typentry->eq_opr))
	{
		if (fc_operator == fc_typentry->eq_opr)
			fc_isEquality = true;
		else if (get_negator(fc_operator) == fc_typentry->eq_opr)
			fc_isInequality = true;
	}

	/*
	 * 如果它是相等或不相等，我们可能能把它估算为
	 * 一种数组包含关系；例如“const = ANY(column)”可以被
	 * 视为“ARRAY[const] <@ column”。scalararraysel_containment
	 * 尝试这样做，如果成功则返回选择性估计，失败则返回-1。
	 */
	if ((fc_isEquality || fc_isInequality) && !fc_is_join_clause)
	{
		fc_s1 = scalararraysel_containment(fc_root, fc_leftop, fc_rightop,
										fc_nominal_element_type,
										fc_isEquality, fc_useOr, fc_varRelid);
		if (fc_s1 >= 0.0)
			return fc_s1;
	}

	/*
	 * 查找基础操作符的选择性估计器。如果没有，就放弃。
	 */
	if (fc_is_join_clause)
		fc_oprsel = get_oprjoin(fc_operator);
	else
		fc_oprsel = get_oprrest(fc_operator);
	if (!fc_oprsel)
		return (Selectivity) 0.5;
	fmgr_info(fc_oprsel, &fc_oprselproc);

	/*
	 * 在上面的数组包含性检查中，我们必须只相信一个操作符是相等或不相等，
	 * 如果它是元素类型的默认btree相等操作符（或其否定），因为这些是
	 * 数组包含性将使用的操作符。但是在接下来的内容中，我们可以
	 * 稍微放宽一些，也相信任何使用eqsel()或neqsel()作为选择性
	 * 估算器的操作符表现得像相等或不相等。
	 */
	if (fc_oprsel == F_EQSEL || fc_oprsel == F_EQJOINSEL)
		fc_isEquality = true;
	else if (fc_oprsel == F_NEQSEL || fc_oprsel == F_NEQJOINSEL)
		fc_isInequality = true;

	/*
	 * 我们考虑三种情况：
	 *
	 * 1. rightop是一个数组常量：解构数组，为每个数组元素应用
	 * 操作符的选择性函数，并以clausesel.c对AND/OR组合的处理方式合并结果。
	 *
	 * 2. rightop是一个ARRAY[]构造：对ARRAY[]构造的每个元素应用
	 * 操作符的选择性函数并进行合并。
	 *
	 * 3. 否则，做一个猜测...
	 */
	if (fc_rightop && IsA(fc_rightop, Const))
	{
		Datum		fc_arraydatum = ((Const *) fc_rightop)->constvalue;
		bool		fc_arrayisnull = ((Const *) fc_rightop)->constisnull;
		ArrayType  *fc_arrayval;
		int16		fc_elmlen;
		bool		fc_elmbyval;
		char		fc_elmalign;
		int			fc_num_elems;
		Datum	   *fc_elem_values;
		bool	   *fc_elem_nulls;
		int			fc_i;

		if (fc_arrayisnull)		/* 如果数组为null，qual无法成功 */
			return (Selectivity) 0.0;
		fc_arrayval = DatumGetArrayTypeP(fc_arraydatum);
		get_typlenbyvalalign(ARR_ELEMTYPE(fc_arrayval),
							 &fc_elmlen, &fc_elmbyval, &fc_elmalign);
		deconstruct_array(fc_arrayval,
						  ARR_ELEMTYPE(fc_arrayval),
						  fc_elmlen, fc_elmbyval, fc_elmalign,
						  &fc_elem_values, &fc_elem_nulls, &fc_num_elems);

		/*
		 * 对于通用操作符，我们假设成功的概率对于每个数组元素是
		 * 独立的。但对于“= ANY”或“<> ALL”，如果数组元素是不同的
		 * （这通常是情况），那么概率是互斥的，我们只需将它们相加。
		 *
		 * 如果我们真的很紧张，我们会尝试确认这些元素都是不同的，
		 * 但这似乎代价昂贵，而且似乎并不值得花费周期；这将意味着
		 * 惩罚良好编写的查询而让糟糕编写的查询受益。然而，我们
		 * 通过检查互斥假设是否导致不可能（超出范围）的概率来
		 * 保护自己一点；如果是这样，我们将退回到正常的计算。
		 */
		fc_s1 = fc_s1disjoint = (fc_useOr ? 0.0 : 1.0);

		for (fc_i = 0; fc_i < fc_num_elems; fc_i++)
		{
			List	   *fc_args;
			Selectivity fc_s2;

			fc_args = list_make2(fc_leftop,
							  makeConst(fc_nominal_element_type,
										-1,
										fc_nominal_element_collation,
										fc_elmlen,
										fc_elem_values[fc_i],
										fc_elem_nulls[fc_i],
										fc_elmbyval));
			if (fc_is_join_clause)
				fc_s2 = DatumGetFloat8(FunctionCall5Coll(&fc_oprselproc,
													  fc_clause->inputcollid,
													  PointerGetDatum(fc_root),
													  ObjectIdGetDatum(fc_operator),
													  PointerGetDatum(fc_args),
													  Int16GetDatum(fc_jointype),
													  PointerGetDatum(fc_sjinfo)));
			else
				fc_s2 = DatumGetFloat8(FunctionCall4Coll(&fc_oprselproc,
													  fc_clause->inputcollid,
													  PointerGetDatum(fc_root),
													  ObjectIdGetDatum(fc_operator),
													  PointerGetDatum(fc_args),
													  Int32GetDatum(fc_varRelid)));

			if (fc_useOr)
			{
				fc_s1 = fc_s1 + fc_s2 - fc_s1 * fc_s2;
				if (fc_isEquality)
					fc_s1disjoint += fc_s2;
			}
			else
			{
				fc_s1 = fc_s1 * fc_s2;
				if (fc_isInequality)
					fc_s1disjoint += fc_s2 - 1.0;
			}
		}

		/* 如果在范围内则接受互斥概率估计 */
		if ((fc_useOr ? fc_isEquality : fc_isInequality) &&
			fc_s1disjoint >= 0.0 && fc_s1disjoint <= 1.0)
			fc_s1 = fc_s1disjoint;
	}
	else if (fc_rightop && IsA(fc_rightop, ArrayExpr) &&
			 !((ArrayExpr *) fc_rightop)->multidims)
	{
		ArrayExpr  *fc_arrayexpr = (ArrayExpr *) fc_rightop;
		int16		fc_elmlen;
		bool		fc_elmbyval;
		ListCell   *fc_l;

		get_typlenbyval(fc_arrayexpr->element_typeid,
						&fc_elmlen, &fc_elmbyval);

		/*
		 * 我们在这里也使用互斥概率的假设，尽管如果元素不是
		 * 全部常量，等于数组元素的概率将相对更高（如果它们
		 * 不是，常量折叠将会将ArrayExpr减小到Const）。在这条路径上，
		 * 在s1disjoint估计上进行合理性检查是至关重要的。
		 */
		fc_s1 = fc_s1disjoint = (fc_useOr ? 0.0 : 1.0);

		foreach(fc_l, fc_arrayexpr->elements)
		{
			Node	   *fc_elem = (Node *) lfirst(fc_l);
			List	   *fc_args;
			Selectivity fc_s2;

			/*
			 * 从理论上讲，如果elem不是nominal_element_type，我们应该
			 * 插入一个RelabelType，但看起来没有哪个操作符
			 * 估计函数会真正在意...
			 */
			fc_args = list_make2(fc_leftop, fc_elem);
			if (fc_is_join_clause)
				fc_s2 = DatumGetFloat8(FunctionCall5Coll(&fc_oprselproc,
													  fc_clause->inputcollid,
													  PointerGetDatum(fc_root),
													  ObjectIdGetDatum(fc_operator),
													  PointerGetDatum(fc_args),
													  Int16GetDatum(fc_jointype),
													  PointerGetDatum(fc_sjinfo)));
			else
				fc_s2 = DatumGetFloat8(FunctionCall4Coll(&fc_oprselproc,
													  fc_clause->inputcollid,
													  PointerGetDatum(fc_root),
													  ObjectIdGetDatum(fc_operator),
													  PointerGetDatum(fc_args),
													  Int32GetDatum(fc_varRelid)));

			if (fc_useOr)
			{
				fc_s1 = fc_s1 + fc_s2 - fc_s1 * fc_s2;
				if (fc_isEquality)
					fc_s1disjoint += fc_s2;
			}
			else
			{
				fc_s1 = fc_s1 * fc_s2;
				if (fc_isInequality)
					fc_s1disjoint += fc_s2 - 1.0;
			}
		}

		/* 如果在范围内则接受互斥概率估计 */
		if ((fc_useOr ? fc_isEquality : fc_isInequality) &&
			fc_s1disjoint >= 0.0 && fc_s1disjoint <= 1.0)
			fc_s1 = fc_s1disjoint;
	}
	else
	{
		CaseTestExpr *fc_dummyexpr;
		List	   *fc_args;
		Selectivity fc_s2;
		int			fc_i;

		/*
		 * 我们需要一个虚拟的rightop传递给操作符选择性
		 * 例程。它可以是几乎任何不看起来像常量的东西；
		 * CaseTestExpr是一个方便的选择。
		 */
		fc_dummyexpr = makeNode(CaseTestExpr);
		fc_dummyexpr->typeId = fc_nominal_element_type;
		fc_dummyexpr->typeMod = -1;
		fc_dummyexpr->collation = fc_clause->inputcollid;
		fc_args = list_make2(fc_leftop, fc_dummyexpr);
		if (fc_is_join_clause)
			fc_s2 = DatumGetFloat8(FunctionCall5Coll(&fc_oprselproc,
												  fc_clause->inputcollid,
												  PointerGetDatum(fc_root),
												  ObjectIdGetDatum(fc_operator),
												  PointerGetDatum(fc_args),
												  Int16GetDatum(fc_jointype),
												  PointerGetDatum(fc_sjinfo)));
		else
			fc_s2 = DatumGetFloat8(FunctionCall4Coll(&fc_oprselproc,
												  fc_clause->inputcollid,
												  PointerGetDatum(fc_root),
												  ObjectIdGetDatum(fc_operator),
												  PointerGetDatum(fc_args),
												  Int32GetDatum(fc_varRelid)));
		fc_s1 = fc_useOr ? 0.0 : 1.0;

		/*
		 * 任意假设最终数组值中有10个元素（参见
		 * estimate_array_length）。在这里我们不冒假设互斥概率的风险。
		 */
		for (fc_i = 0; fc_i < 10; fc_i++)
		{
			if (fc_useOr)
				fc_s1 = fc_s1 + fc_s2 - fc_s1 * fc_s2;
			else
				fc_s1 = fc_s1 * fc_s2;
		}
	}

	/* 结果应该在范围内，但确保... */
	CLAMP_PROBABILITY(fc_s1);

	return fc_s1;
}

/*
 * 估算由表达式产生的数组中的元素数量。
 *
 * 确保这与scalararraysel一致是重要的。
 */
int estimate_array_length(Node *fc_arrayexpr)
{
	/* 查看arrayexpr的任何二进制兼容重新标记 */
	fc_arrayexpr = fc_strip_array_coercion(fc_arrayexpr);

	if (fc_arrayexpr && IsA(fc_arrayexpr, Const))
	{
		Datum		fc_arraydatum = ((Const *) fc_arrayexpr)->constvalue;
		bool		fc_arrayisnull = ((Const *) fc_arrayexpr)->constisnull;
		ArrayType  *fc_arrayval;

		if (fc_arrayisnull)
			return 0;
		fc_arrayval = DatumGetArrayTypeP(fc_arraydatum);
		return ArrayGetNItems(ARR_NDIM(fc_arrayval), ARR_DIMS(fc_arrayval));
	}
	else if (fc_arrayexpr && IsA(fc_arrayexpr, ArrayExpr) &&
			 !((ArrayExpr *) fc_arrayexpr)->multidims)
	{
		return list_length(((ArrayExpr *) fc_arrayexpr)->elements);
	}
	else
	{
		/* 默认猜测 --- 另见scalararraysel */
		return 10;
	}
}

/*
 *		rowcomparesel		- RowCompareExpr节点的选择性。
 *
 * 我们通过仅考虑第一个（高
 * 位）列来估算RowCompare的选择性，这使得它等同于普通的OpExpr。
 * 虽然通过考虑额外的列可以对这个估算进行细化，但没有多列
 * 统计信息，我们似乎无法做得更好。
 */
Selectivity rowcomparesel(PlannerInfo *fc_root,
			  RowCompareExpr *fc_clause,
			  int fc_varRelid, JoinType fc_jointype, SpecialJoinInfo *fc_sjinfo)
{
	Selectivity fc_s1;
	Oid			fc_opno = linitial_oid(fc_clause->opnos);
	Oid			fc_inputcollid = linitial_oid(fc_clause->inputcollids);
	List	   *fc_opargs;
	bool		fc_is_join_clause;

	/* 为单个操作符构建等效的参数列表 */
	fc_opargs = list_make2(linitial(fc_clause->largs), linitial(fc_clause->rargs));

	/*
	 * 决定它是否是连接子句。这应该与clausesel.c的
	 * treat_as_join_clause()匹配，除了我们故意只考虑
	 * 领先的列而不是子句的其余部分。
	 */
	if (fc_varRelid != 0)
	{
		/*
		 * 调用者正在强制限制模式（例如，因为我们正在检查一个
		 * 内部索引扫描条件）。
		 */
		fc_is_join_clause = false;
	}
	else if (fc_sjinfo == NULL)
	{
		/*
		 * 它必须是一个限制子句，因为它是在扫描节点上评估的。
		 */
		fc_is_join_clause = false;
	}
	else
	{
		/*
		 * 否则，如果使用了多个关系，则它是一个连接。
		 */
		fc_is_join_clause = (NumRelids(fc_root, (Node *) fc_opargs) > 1);
	}

	if (fc_is_join_clause)
	{
		/* 估算连接子句的选择性。 */
		fc_s1 = join_selectivity(fc_root, fc_opno,
							  fc_opargs,
							  fc_inputcollid,
							  fc_jointype,
							  fc_sjinfo);
	}
	else
	{
		/* 估算限制子句的选择性。 */
		fc_s1 = restriction_selectivity(fc_root, fc_opno,
									 fc_opargs,
									 fc_inputcollid,
									 fc_varRelid);
	}

	return fc_s1;
}

/*
 *		eqjoinsel		- "="的连接选择性
 */
Datum eqjoinsel(PG_FUNCTION_ARGS)
{
	PlannerInfo *fc_root = (PlannerInfo *) PG_GETARG_POINTER(0);
	Oid			fc_operator = PG_GETARG_OID(1);
	List	   *fc_args = (List *) PG_GETARG_POINTER(2);

#ifdef NOT_USED
	JoinType	fc_jointype = (JoinType) PG_GETARG_INT16(3);
#endif
	SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) PG_GETARG_POINTER(4);
	Oid			fc_collation = PG_GET_COLLATION();
	double		fc_selec;
	double		fc_selec_inner;
	VariableStatData fc_vardata1;
	VariableStatData fc_vardata2;
	double		fc_nd1;
	double		fc_nd2;
	bool		fc_isdefault1;
	bool		fc_isdefault2;
	Oid			fc_opfuncoid;
	AttStatsSlot fc_sslot1;
	AttStatsSlot fc_sslot2;
	Form_pg_statistic fc_stats1 = NULL;
	Form_pg_statistic fc_stats2 = NULL;
	bool		fc_have_mcvs1 = false;
	bool		fc_have_mcvs2 = false;
	bool		fc_join_is_reversed;
	RelOptInfo *fc_inner_rel;

	get_join_variables(fc_root, fc_args, fc_sjinfo,
					   &fc_vardata1, &fc_vardata2, &fc_join_is_reversed);

	fc_nd1 = get_variable_numdistinct(&fc_vardata1, &fc_isdefault1);
	fc_nd2 = get_variable_numdistinct(&fc_vardata2, &fc_isdefault2);

	fc_opfuncoid = get_opcode(fc_operator);

	memset(&fc_sslot1, 0, sizeof(fc_sslot1));
	memset(&fc_sslot2, 0, sizeof(fc_sslot2));

	if (HeapTupleIsValid(fc_vardata1.statsTuple))
	{
		/* 注意我们允许使用nullfrac，而不考虑安全检查 */
		fc_stats1 = (Form_pg_statistic) GETSTRUCT(fc_vardata1.statsTuple);
		if (statistic_proc_security_check(&fc_vardata1, fc_opfuncoid))
			fc_have_mcvs1 = get_attstatsslot(&fc_sslot1, fc_vardata1.statsTuple,
										  STATISTIC_KIND_MCV, InvalidOid,
										  ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS);
	}

	if (HeapTupleIsValid(fc_vardata2.statsTuple))
	{
		/* 注意我们允许使用nullfrac，而不考虑安全检查 */
		fc_stats2 = (Form_pg_statistic) GETSTRUCT(fc_vardata2.statsTuple);
		if (statistic_proc_security_check(&fc_vardata2, fc_opfuncoid))
			fc_have_mcvs2 = get_attstatsslot(&fc_sslot2, fc_vardata2.statsTuple,
										  STATISTIC_KIND_MCV, InvalidOid,
										  ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS);
	}

	/* 在所有情况下我们需要计算内连接选择性 */
	fc_selec_inner = fc_eqjoinsel_inner(fc_opfuncoid, fc_collation,
								  &fc_vardata1, &fc_vardata2,
								  fc_nd1, fc_nd2,
								  fc_isdefault1, fc_isdefault2,
								  &fc_sslot1, &fc_sslot2,
								  fc_stats1, fc_stats2,
								  fc_have_mcvs1, fc_have_mcvs2);

	switch (fc_sjinfo->jointype)
	{
		case JOIN_INNER:
		case JOIN_LEFT:
		case JOIN_FULL:
			fc_selec = fc_selec_inner;
			break;
		case JOIN_SEMI:
		case JOIN_ANTI:

			/*
			 * 查找连接的内部关系。min_righthand足够的
			 * 信息，因为SEMI或ANTI连接都不允许对其RHS进行任何
			 * 重关联，因此右侧将始终正好是那组关系。
			 */
			fc_inner_rel = fc_find_join_input_rel(fc_root, fc_sjinfo->min_righthand);

			if (!fc_join_is_reversed)
				fc_selec = fc_eqjoinsel_semi(fc_opfuncoid, fc_collation,
									   &fc_vardata1, &fc_vardata2,
									   fc_nd1, fc_nd2,
									   fc_isdefault1, fc_isdefault2,
									   &fc_sslot1, &fc_sslot2,
									   fc_stats1, fc_stats2,
									   fc_have_mcvs1, fc_have_mcvs2,
									   fc_inner_rel);
			else
			{
				Oid			fc_commop = get_commutator(fc_operator);
				Oid			fc_commopfuncoid = OidIsValid(fc_commop) ? get_opcode(fc_commop) : InvalidOid;

				fc_selec = fc_eqjoinsel_semi(fc_commopfuncoid, fc_collation,
									   &fc_vardata2, &fc_vardata1,
									   fc_nd2, fc_nd1,
									   fc_isdefault2, fc_isdefault1,
									   &fc_sslot2, &fc_sslot1,
									   fc_stats2, fc_stats1,
									   fc_have_mcvs2, fc_have_mcvs1,
									   fc_inner_rel);
			}

			/*
			 * 我们绝不能估算半连接的输出为比我们对具有相同输入
			 * 关系和连接条件的内连接的估算更多的行；这显然是不可能
			 * 发生的。前者的估算是N1 * Ssemi，而后者是
			 * N1 * N2 * Sinner，因此我们可以将Ssemi限制为
			 * N2 * Sinner。这样做是值得的，因为在eqjoinsel_semi的
			 * 估算规则比较不稳定的情况下，特别是在必须完全推测的
			 * 情况下。
			 */
			fc_selec = Min(fc_selec, fc_inner_rel->rows * fc_selec_inner);
			break;
		default:
			/* 此处不期望其他值 */
			elog(ERROR, "unrecognized join type: %d",
				 (int) fc_sjinfo->jointype);
			fc_selec = 0;			/* 保持编译器安静 */
			break;
	}

	free_attstatsslot(&fc_sslot1);
	free_attstatsslot(&fc_sslot2);

	ReleaseVariableStats(fc_vardata1);
	ReleaseVariableStats(fc_vardata2);

	CLAMP_PROBABILITY(fc_selec);

	PG_RETURN_FLOAT8((float8) fc_selec);
}

/*
 * eqjoinsel_inner --- 正常内连接的eqjoinsel
 *
 * 我们也将其用于LEFT/FULL外连接；目前尚不清楚
 * 在这里尝试区分它们是否值得。
 */
static double fc_eqjoinsel_inner(Oid fc_opfuncoid, Oid fc_collation,
				VariableStatData *fc_vardata1, VariableStatData *fc_vardata2,
				double fc_nd1, double fc_nd2,
				bool fc_isdefault1, bool fc_isdefault2,
				AttStatsSlot *fc_sslot1, AttStatsSlot *fc_sslot2,
				Form_pg_statistic fc_stats1, Form_pg_statistic fc_stats2,
				bool fc_have_mcvs1, bool fc_have_mcvs2)
{
	double		fc_selec;

	if (fc_have_mcvs1 && fc_have_mcvs2)
	{
		
/*
		 * 我们有两个关联的最常见值列表。遍历
		 * 列表以查看哪些 MCV 实际上与给定操作符相互连接。这样可以确定 MCV
		 * 列表所代表的关系部分的确切连接选择性。我们仍然需要对剩余数据进行估计，但
		 * 在偏斜分布中，这大大提高了准确性。
		 * 有关动机，请参见 Y. Ioannidis 和 S.
		 * Christodoulakis 的分析，“关于连接结果大小中错误的传播”，技术报告 1018，计算机科学系，威斯康星大学，麦迪逊，1991 年 3 月（可从 ftp.cs.wisc.edu 获取）。
		 */
		LOCAL_FCINFO(fcinfo, 2);
		FmgrInfo	fc_eqproc;
		bool	   *fc_hasmatch1;
		bool	   *fc_hasmatch2;
		double		fc_nullfrac1 = fc_stats1->stanullfrac;
		double		fc_nullfrac2 = fc_stats2->stanullfrac;
		double		fc_matchprodfreq,
					fc_matchfreq1,
					fc_matchfreq2,
					fc_unmatchfreq1,
					fc_unmatchfreq2,
					fc_otherfreq1,
					fc_otherfreq2,
					fc_totalsel1,
					fc_totalsel2;
		int			fc_i,
					fc_nmatches;

		fmgr_info(fc_opfuncoid, &fc_eqproc);

		/*
		 * 通过只设置一次 fcinfo 结构节省一些周期。直接使用
		 * FunctionCallInvoke 也避免了在 eqproc
		 * 返回 NULL 时出现故障，尽管实际上相等函数不应这样做。
		 */
		InitFunctionCallInfoData(*fcinfo, &fc_eqproc, 2, fc_collation,
								 NULL, NULL);
		fcinfo->args[0].isnull = false;
		fcinfo->args[1].isnull = false;

		fc_hasmatch1 = (bool *) palloc0(fc_sslot1->nvalues * sizeof(bool));
		fc_hasmatch2 = (bool *) palloc0(fc_sslot2->nvalues * sizeof(bool));

		/*
		 * 注意，我们假设每个 MCV 至多匹配其他 MCV
		 * 列表的一个成员。如果操作符实际上不是相等，则可能
		 * 有多个匹配——但我们不会寻找它们，这既是为了速度
		 * 也是因为数学不成立...
		 */
		fc_matchprodfreq = 0.0;
		fc_nmatches = 0;
		for (fc_i = 0; fc_i < fc_sslot1->nvalues; fc_i++)
		{
			int			fc_j;

			fcinfo->args[0].value = fc_sslot1->values[fc_i];

			for (fc_j = 0; fc_j < fc_sslot2->nvalues; fc_j++)
			{
				Datum		fc_fresult;

				if (fc_hasmatch2[fc_j])
					continue;
				fcinfo->args[1].value = fc_sslot2->values[fc_j];
				fcinfo->isnull = false;
				fc_fresult = FunctionCallInvoke(fcinfo);
				if (!fcinfo->isnull && DatumGetBool(fc_fresult))
				{
					fc_hasmatch1[fc_i] = fc_hasmatch2[fc_j] = true;
					fc_matchprodfreq += fc_sslot1->numbers[fc_i] * fc_sslot2->numbers[fc_j];
					fc_nmatches++;
					break;
				}
			}
		}
		CLAMP_PROBABILITY(fc_matchprodfreq);
		/* 汇总匹配和未匹配 MCV 的频率 */
		fc_matchfreq1 = fc_unmatchfreq1 = 0.0;
		for (fc_i = 0; fc_i < fc_sslot1->nvalues; fc_i++)
		{
			if (fc_hasmatch1[fc_i])
				fc_matchfreq1 += fc_sslot1->numbers[fc_i];
			else
				fc_unmatchfreq1 += fc_sslot1->numbers[fc_i];
		}
		CLAMP_PROBABILITY(fc_matchfreq1);
		CLAMP_PROBABILITY(fc_unmatchfreq1);
		fc_matchfreq2 = fc_unmatchfreq2 = 0.0;
		for (fc_i = 0; fc_i < fc_sslot2->nvalues; fc_i++)
		{
			if (fc_hasmatch2[fc_i])
				fc_matchfreq2 += fc_sslot2->numbers[fc_i];
			else
				fc_unmatchfreq2 += fc_sslot2->numbers[fc_i];
		}
		CLAMP_PROBABILITY(fc_matchfreq2);
		CLAMP_PROBABILITY(fc_unmatchfreq2);
		pfree(fc_hasmatch1);
		pfree(fc_hasmatch2);

		/*
		 * 计算不在 MCV
		 * 列表中的非空值的总频率。
		 */
		fc_otherfreq1 = 1.0 - fc_nullfrac1 - fc_matchfreq1 - fc_unmatchfreq1;
		fc_otherfreq2 = 1.0 - fc_nullfrac2 - fc_matchfreq2 - fc_unmatchfreq2;
		CLAMP_PROBABILITY(fc_otherfreq1);
		CLAMP_PROBABILITY(fc_otherfreq2);

		/*
		 * 从关系 1 的角度看，我们可以估计总选择性为：已知的匹配 MCV 的选择性，
		 * 加上假设与关系 2 的非 MCV 人口随机成员匹配的未匹配 MCV，
		 * 加上假设与关系 2 的未匹配 MCV 加上非 MCV 值随机成员匹配的不 MCV 值。
		 */
		fc_totalsel1 = fc_matchprodfreq;
		if (fc_nd2 > fc_sslot2->nvalues)
			fc_totalsel1 += fc_unmatchfreq1 * fc_otherfreq2 / (fc_nd2 - fc_sslot2->nvalues);
		if (fc_nd2 > fc_nmatches)
			fc_totalsel1 += fc_otherfreq1 * (fc_otherfreq2 + fc_unmatchfreq2) /
				(fc_nd2 - fc_nmatches);
		/* 从关系 2 的角度看相同的估计。 */
		fc_totalsel2 = fc_matchprodfreq;
		if (fc_nd1 > fc_sslot1->nvalues)
			fc_totalsel2 += fc_unmatchfreq2 * fc_otherfreq1 / (fc_nd1 - fc_sslot1->nvalues);
		if (fc_nd1 > fc_nmatches)
			fc_totalsel2 += fc_otherfreq2 * (fc_otherfreq1 + fc_unmatchfreq1) /
				(fc_nd1 - fc_nmatches);

		/*
		 * 使用两个估计中较小的一个。这在
		 * 本质上与下面为无统计情况给出的理由相同：初步估计
		 * 我们是从 nd 较小的关系的角度进行估计的。
		 */
		fc_selec = (fc_totalsel1 < fc_totalsel2) ? fc_totalsel1 : fc_totalsel2;
	}
	else
	{
		/*
		 * 我们没有两边的MCV列表。估计连接的选择性为MIN(1/nd1,1/nd2)*(1-nullfrac1)*(1-nullfrac2)。如果我们假设连接操作符是严格的，并且非空值大致均匀分布，这个估计是合理的：给定的非空元组rel1将连接到rel2的零行或N2*(1-nullfrac2)/nd2行，因此总连接行最多为
		 * N1*(1-nullfrac1)*N2*(1-nullfrac2)/nd2，给出的连接选择性不超过(1-nullfrac1)*(1-nullfrac2)/nd2。同样的逻辑意味着它不超过(1-nullfrac1)*(1-nullfrac2)/nd1，因此包含MIN()的表达式是一个上界。使用MIN()意味着我们是从具有较小nd的关系的角度进行估计（因为较大的nd决定了MIN）。合理的假设是这个关系中的大多数元组会有连接伙伴，因此这个界限大概率是合理的，并应按原样接受。
		 *
		 * XXX 如果我们只有一方的MCV列表，我们能否更聪明一些？似乎如果我们假设另一侧是均匀分布的，我们无论如何都会得到相同的答案。
		 */
		double		fc_nullfrac1 = fc_stats1 ? fc_stats1->stanullfrac : 0.0;
		double		fc_nullfrac2 = fc_stats2 ? fc_stats2->stanullfrac : 0.0;

		fc_selec = (1.0 - fc_nullfrac1) * (1.0 - fc_nullfrac2);
		if (fc_nd1 > fc_nd2)
			fc_selec /= fc_nd1;
		else
			fc_selec /= fc_nd2;
	}

	return fc_selec;
}

/*
 * eqjoinsel_semi --- 半连接的eqjoinsel
 *
 * （也用于反连接，我们应该以相同的方式进行估计。）
 * 调用者已确保vardata1是左侧变量。
 * 与eqjoinsel_inner不同，我们必须应对opfuncoid为InvalidOid的情况。
 */
static double fc_eqjoinsel_semi(Oid fc_opfuncoid, Oid fc_collation,
			   VariableStatData *fc_vardata1, VariableStatData *fc_vardata2,
			   double fc_nd1, double fc_nd2,
			   bool fc_isdefault1, bool fc_isdefault2,
			   AttStatsSlot *fc_sslot1, AttStatsSlot *fc_sslot2,
			   Form_pg_statistic fc_stats1, Form_pg_statistic fc_stats2,
			   bool fc_have_mcvs1, bool fc_have_mcvs2,
			   RelOptInfo *fc_inner_rel)
{
	double		fc_selec;

	/*
	 * 我们将nd2限制为不超过我们估计的内关系的大小。这在直观上是合理的，因为显然不可能有比内关系提供的不同值还要多的值。非对称的原因（即我们不限制nd1）是这是限制子句应用于内关系影响连接结果大小估计的唯一路径，因为set_joinrel_size_estimates将SEMI/ANTI选择性仅乘以外部关系的大小。如果我们限制nd1，我们会将外部关系限制的选择性重复计算。
	 *
	 * 我们可以将这种限制同时应用于连接变量来源的基本关系（如果只有一个）以及当前连接的直接内部输入关系。
	 *
	 * 如果我们限制，则可以将nd2视为非默认估计；这也许不是很好，但也不是凭空而来的。当内关系为空且因此没有统计信息时，这最为有用。
	 */
	if (fc_vardata2->rel)
	{
		if (fc_nd2 >= fc_vardata2->rel->rows)
		{
			fc_nd2 = fc_vardata2->rel->rows;
			fc_isdefault2 = false;
		}
	}
	if (fc_nd2 >= fc_inner_rel->rows)
	{
		fc_nd2 = fc_inner_rel->rows;
		fc_isdefault2 = false;
	}

	if (fc_have_mcvs1 && fc_have_mcvs2 && OidIsValid(fc_opfuncoid))
	{
		/*
		 * 我们对于两个关系都有最常见值列表。遍历这些列表以查看哪些MCV实际上在给定操作符下相互连接。这使我们能够确定由MCV列表表示的关系部分的确切连接选择性。我们仍然需要对剩余的人群进行估计，但在偏斜分布中，这为我们的准确性提供了很大的帮助。
		 */
		LOCAL_FCINFO(fcinfo, 2);
		FmgrInfo	fc_eqproc;
		bool	   *fc_hasmatch1;
		bool	   *fc_hasmatch2;
		double		fc_nullfrac1 = fc_stats1->stanullfrac;
		double		fc_matchfreq1,
					fc_uncertainfrac,
					fc_uncertain;
		int			fc_i,
					fc_nmatches,
					fc_clamped_nvalues2;

		/*
		 * 上述的限制可能导致nd2小于sslot2->nvalues；在这种情况下，我们假设正是nd2最常见的值将出现在连接输入中，因此仅与MCV列表的前nd2个成员进行比较。当然，这通常是错误的，但这是我们能做的最佳判断。
		 */
		fc_clamped_nvalues2 = Min(fc_sslot2->nvalues, fc_nd2);

		fmgr_info(fc_opfuncoid, &fc_eqproc);

		/*
		 * 通过只设置一次 fcinfo 结构节省一些周期。直接使用
		 * FunctionCallInvoke 也避免了在 eqproc
		 * 返回 NULL 时出现故障，尽管实际上相等函数不应这样做。
		 */
		InitFunctionCallInfoData(*fcinfo, &fc_eqproc, 2, fc_collation,
								 NULL, NULL);
		fcinfo->args[0].isnull = false;
		fcinfo->args[1].isnull = false;

		fc_hasmatch1 = (bool *) palloc0(fc_sslot1->nvalues * sizeof(bool));
		fc_hasmatch2 = (bool *) palloc0(fc_clamped_nvalues2 * sizeof(bool));

		/*
		 * 注意，我们假设每个 MCV 至多匹配其他 MCV
		 * 列表的一个成员。如果操作符实际上不是相等，则可能
		 * 有多个匹配——但我们不会寻找它们，这既是为了速度
		 * 也是因为数学不成立...
		 */
		fc_nmatches = 0;
		for (fc_i = 0; fc_i < fc_sslot1->nvalues; fc_i++)
		{
			int			fc_j;

			fcinfo->args[0].value = fc_sslot1->values[fc_i];

			for (fc_j = 0; fc_j < fc_clamped_nvalues2; fc_j++)
			{
				Datum		fc_fresult;

				if (fc_hasmatch2[fc_j])
					continue;
				fcinfo->args[1].value = fc_sslot2->values[fc_j];
				fcinfo->isnull = false;
				fc_fresult = FunctionCallInvoke(fcinfo);
				if (!fcinfo->isnull && DatumGetBool(fc_fresult))
				{
					fc_hasmatch1[fc_i] = fc_hasmatch2[fc_j] = true;
					fc_nmatches++;
					break;
				}
			}
		}
		/* 汇总匹配的MCV的频率 */
		fc_matchfreq1 = 0.0;
		for (fc_i = 0; fc_i < fc_sslot1->nvalues; fc_i++)
		{
			if (fc_hasmatch1[fc_i])
				fc_matchfreq1 += fc_sslot1->numbers[fc_i];
		}
		CLAMP_PROBABILITY(fc_matchfreq1);
		pfree(fc_hasmatch1);
		pfree(fc_hasmatch2);

		/*
		 * 现在我们需要估计关系1中至少有一个连接伙伴的部分。我们可以确定匹配的MCV确实有，因此这给了我们一个下界，但我们对其他所有部分确实一无所知。我们粗略的方法是：如果nd1 <= nd2，则假设所有非空的rel1行都有连接伙伴，否则假设不确定的行中有一部分nd2/nd1有连接伙伴。在进行除法之前，我们可以从不同值计数中排除已知匹配的MCV。
		 *
		 * 尽管上述方法粗糙，但如果我们没有可靠的两边的ndistinct值，这完全没有用。因此，如果nd1或nd2其中之一是默认值，则放弃，假设一半的不确定行有连接伙伴。
		 */
		if (!fc_isdefault1 && !fc_isdefault2)
		{
			fc_nd1 -= fc_nmatches;
			fc_nd2 -= fc_nmatches;
			if (fc_nd1 <= fc_nd2 || fc_nd2 < 0)
				fc_uncertainfrac = 1.0;
			else
				fc_uncertainfrac = fc_nd2 / fc_nd1;
		}
		else
			fc_uncertainfrac = 0.5;
		fc_uncertain = 1.0 - fc_matchfreq1 - fc_nullfrac1;
		CLAMP_PROBABILITY(fc_uncertain);
		fc_selec = fc_matchfreq1 + fc_uncertainfrac * fc_uncertain;
	}
	else
	{
		/*
		 * 如果没有两边的MCV列表，我们只能使用有关nd1与nd2的启发式方法。
		 */
		double		fc_nullfrac1 = fc_stats1 ? fc_stats1->stanullfrac : 0.0;

		if (!fc_isdefault1 && !fc_isdefault2)
		{
			if (fc_nd1 <= fc_nd2 || fc_nd2 < 0)
				fc_selec = 1.0 - fc_nullfrac1;
			else
				fc_selec = (fc_nd2 / fc_nd1) * (1.0 - fc_nullfrac1);
		}
		else
			fc_selec = 0.5 * (1.0 - fc_nullfrac1);
	}

	return fc_selec;
}

/*
 *		neqjoinsel		- “!=”的连接选择性
 */
Datum neqjoinsel(PG_FUNCTION_ARGS)
{
	PlannerInfo *fc_root = (PlannerInfo *) PG_GETARG_POINTER(0);
	Oid			fc_operator = PG_GETARG_OID(1);
	List	   *fc_args = (List *) PG_GETARG_POINTER(2);
	JoinType	fc_jointype = (JoinType) PG_GETARG_INT16(3);
	SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) PG_GETARG_POINTER(4);
	Oid			fc_collation = PG_GET_COLLATION();
	float8		fc_result;

	if (fc_jointype == JOIN_SEMI || fc_jointype == JOIN_ANTI)
	{
		
/*
		 * 对于半连接，如果右侧关系中有多个不同的值，
		 * 那么每个非空的左侧行必须找到一个可以连接的行，
		 * 因为它只能等于其中之一。为了稳定性，我们假设
		 * 右侧总是有多个不同的值，尽管理论上我们可以有
		 * 空右侧（选择性 = 0）和单一不同值右侧（选择性 =
		 * 与单一右侧值相同的左侧的比例）的特例。
		 *
		 * 对于反连接，如果我们使用相同的假设，即右侧关系中
		 * 有多个不同的键，那么每个非空的左侧行必须通过反连接被抑制。
		 *
		 * 所以无论哪种方式，选择性估计应该是 1 - nullfrac。
		 */
		VariableStatData fc_leftvar;
		VariableStatData fc_rightvar;
		bool		fc_reversed;
		HeapTuple	fc_statsTuple;
		double		fc_nullfrac;

		get_join_variables(fc_root, fc_args, fc_sjinfo, &fc_leftvar, &fc_rightvar, &fc_reversed);
		fc_statsTuple = fc_reversed ? fc_rightvar.statsTuple : fc_leftvar.statsTuple;
		if (HeapTupleIsValid(fc_statsTuple))
			fc_nullfrac = ((Form_pg_statistic) GETSTRUCT(fc_statsTuple))->stanullfrac;
		else
			fc_nullfrac = 0.0;
		ReleaseVariableStats(fc_leftvar);
		ReleaseVariableStats(fc_rightvar);

		fc_result = 1.0 - fc_nullfrac;
	}
	else
	{
		/*
		 * 我们想要 1 - eqjoinsel()，其中等式操作符与这个 !=
		 * 操作符相关，即它的取反形式。
		 */
		Oid			fc_eqop = get_negator(fc_operator);

		if (fc_eqop)
		{
			fc_result =
				DatumGetFloat8(DirectFunctionCall5Coll(eqjoinsel,
													   fc_collation,
													   PointerGetDatum(fc_root),
													   ObjectIdGetDatum(fc_eqop),
													   PointerGetDatum(fc_args),
													   Int16GetDatum(fc_jointype),
													   PointerGetDatum(fc_sjinfo)));
		}
		else
		{
			/* 使用默认选择性（我们应该提高错误吗？） */
			fc_result = DEFAULT_EQ_SEL;
		}
		fc_result = 1.0 - fc_result;
	}

	PG_RETURN_FLOAT8(fc_result);
}

/*
 *		scalarltjoinsel - 标量的 "<" 连接选择性
 */
Datum scalarltjoinsel(PG_FUNCTION_ARGS)
{
	PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
}

/*
 *		scalarlejoinsel - 标量的 "<=" 连接选择性
 */
Datum scalarlejoinsel(PG_FUNCTION_ARGS)
{
	PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
}

/*
 *		scalargtjoinsel - 标量的 ">" 连接选择性
 */
Datum scalargtjoinsel(PG_FUNCTION_ARGS)
{
	PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
}

/*
 *		scalargejoinsel - 标量的 ">=" 连接选择性
 */
Datum scalargejoinsel(PG_FUNCTION_ARGS)
{
	PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
}


/*
 * mergejoinscansel - 合并连接的扫描选择性。
 *
 * 一旦合并连接耗尽任一输入流，它将停止。
 * 因此，如果我们能够估计两个输入变量的范围，
 * 我们就可以估计将实际读取多少输入。 这
 * 在使用索引扫描时可能对成本有相当大的影响。
 *
 * 此外，我们可以估计在找到第一个连接对之前
 * 每个输入必须读取多少，这将影响连接的启动时间。
 *
 * clause 应该是已经知道可以合并连接的子句。opfamily、
 * strategy 和 nulls_first 指定所使用的排序顺序。
 *
 * 输出是：
 *		*leftstart 被设置为在找到第一个连接对之前
 *		 预计要扫描的左侧变量的比例（0到1）。
 *		*leftend 被设置为在连接终止之前
 *		 预计要扫描的左侧变量的比例（0到1）。
 *		*rightstart、*rightend 对右侧变量类似。
 */
void mergejoinscansel(PlannerInfo *fc_root, Node *fc_clause,
				 Oid fc_opfamily, int fc_strategy, bool fc_nulls_first,
				 Selectivity *fc_leftstart, Selectivity *fc_leftend,
				 Selectivity *fc_rightstart, Selectivity *fc_rightend)
{
	Node	   *fc_left,
			   *fc_right;
	VariableStatData fc_leftvar,
				fc_rightvar;
	int			fc_op_strategy;
	Oid			fc_op_lefttype;
	Oid			fc_op_righttype;
	Oid			fc_opno,
				fc_collation,
				fc_lsortop,
				fc_rsortop,
				fc_lstatop,
				fc_rstatop,
				fc_ltop,
				fc_leop,
				fc_revltop,
				fc_revleop;
	bool		fc_isgt;
	Datum		fc_leftmin,
				fc_leftmax,
				fc_rightmin,
				fc_rightmax;
	double		fc_selec;

	/* 如果我们无法确定任何结果，则设置默认结果。 */
	/* XXX 默认“开始”比例是否应该稍微大于 0？ */
	*fc_leftstart = *fc_rightstart = 0.0;
	*fc_leftend = *fc_rightend = 1.0;

	/* 解构合并子句 */
	if (!is_opclause(fc_clause))
		return;					/* 不应该发生 */
	fc_opno = ((OpExpr *) fc_clause)->opno;
	fc_collation = ((OpExpr *) fc_clause)->inputcollid;
	fc_left = get_leftop((Expr *) fc_clause);
	fc_right = get_rightop((Expr *) fc_clause);
	if (!fc_right)
		return;					/* 不应该发生 */

	/* 查找输入的统计信息 */
	examine_variable(fc_root, fc_left, 0, &fc_leftvar);
	examine_variable(fc_root, fc_right, 0, &fc_rightvar);

	/* 提取操作符声明的左/右数据类型 */
	get_op_opfamily_properties(fc_opno, fc_opfamily, false,
							   &fc_op_strategy,
							   &fc_op_lefttype,
							   &fc_op_righttype);
	Assert(fc_op_strategy == BTEqualStrategyNumber);

	/*
	 * 查找我们需要的各种操作符。如果我们没有找到它们全部，
	 * 这可能意味着 opfamily 是损坏的，但我们只会静默失败。
	 *
	 * 注意：我们期望 pg_statistic 直方图将根据 '<'
	 * 操作符进行排序，无论我们考虑的是哪种排序方向。
	 */
	switch (fc_strategy)
	{
		case BTLessStrategyNumber:
			fc_isgt = false;
			if (fc_op_lefttype == fc_op_righttype)
			{
				/* 简单的情况 */
				fc_ltop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTLessStrategyNumber);
				fc_leop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTLessEqualStrategyNumber);
				fc_lsortop = fc_ltop;
				fc_rsortop = fc_ltop;
				fc_lstatop = fc_lsortop;
				fc_rstatop = fc_rsortop;
				fc_revltop = fc_ltop;
				fc_revleop = fc_leop;
			}
			else
			{
				fc_ltop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTLessStrategyNumber);
				fc_leop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTLessEqualStrategyNumber);
				fc_lsortop = get_opfamily_member(fc_opfamily,
											  fc_op_lefttype, fc_op_lefttype,
											  BTLessStrategyNumber);
				fc_rsortop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_righttype,
											  BTLessStrategyNumber);
				fc_lstatop = fc_lsortop;
				fc_rstatop = fc_rsortop;
				fc_revltop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_lefttype,
											  BTLessStrategyNumber);
				fc_revleop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_lefttype,
											  BTLessEqualStrategyNumber);
			}
			break;
		case BTGreaterStrategyNumber:
			/* 降序情况 */
			fc_isgt = true;
			if (fc_op_lefttype == fc_op_righttype)
			{
				/* 简单的情况 */
				fc_ltop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTGreaterStrategyNumber);
				fc_leop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTGreaterEqualStrategyNumber);
				fc_lsortop = fc_ltop;
				fc_rsortop = fc_ltop;
				fc_lstatop = get_opfamily_member(fc_opfamily,
											  fc_op_lefttype, fc_op_lefttype,
											  BTLessStrategyNumber);
				fc_rstatop = fc_lstatop;
				fc_revltop = fc_ltop;
				fc_revleop = fc_leop;
			}
			else
			{
				fc_ltop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTGreaterStrategyNumber);
				fc_leop = get_opfamily_member(fc_opfamily,
										   fc_op_lefttype, fc_op_righttype,
										   BTGreaterEqualStrategyNumber);
				fc_lsortop = get_opfamily_member(fc_opfamily,
											  fc_op_lefttype, fc_op_lefttype,
											  BTGreaterStrategyNumber);
				fc_rsortop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_righttype,
											  BTGreaterStrategyNumber);
				fc_lstatop = get_opfamily_member(fc_opfamily,
											  fc_op_lefttype, fc_op_lefttype,
											  BTLessStrategyNumber);
				fc_rstatop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_righttype,
											  BTLessStrategyNumber);
				fc_revltop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_lefttype,
											  BTGreaterStrategyNumber);
				fc_revleop = get_opfamily_member(fc_opfamily,
											  fc_op_righttype, fc_op_lefttype,
											  BTGreaterEqualStrategyNumber);
			}
			break;
		default:
			goto fail;			/* 不应该到这里 */
	}

	if (!OidIsValid(fc_lsortop) ||
		!OidIsValid(fc_rsortop) ||
		!OidIsValid(fc_lstatop) ||
		!OidIsValid(fc_rstatop) ||
		!OidIsValid(fc_ltop) ||
		!OidIsValid(fc_leop) ||
		!OidIsValid(fc_revltop) ||
		!OidIsValid(fc_revleop))
		goto fail;				/* 目录中的信息不足 */

	/* 尝试获取两个输入的范围 */
	if (!fc_isgt)
	{
		if (!get_variable_range(fc_root, &fc_leftvar, fc_lstatop, fc_collation,
								&fc_leftmin, &fc_leftmax))
			goto fail;			/* 从统计信息中没有可用范围 */
		if (!get_variable_range(fc_root, &fc_rightvar, fc_rstatop, fc_collation,
								&fc_rightmin, &fc_rightmax))
			goto fail;			/* 从统计信息中没有可用范围 */
	}
	else
	{
		/* 需要交换最大值和最小值 */
		if (!get_variable_range(fc_root, &fc_leftvar, fc_lstatop, fc_collation,
								&fc_leftmax, &fc_leftmin))
			goto fail;			/* 从统计信息中没有可用范围 */
		if (!get_variable_range(fc_root, &fc_rightvar, fc_rstatop, fc_collation,
								&fc_rightmax, &fc_rightmin))
			goto fail;			/* 从统计信息中没有可用范围 */
	}

	/*
	 * 现在，左侧变量将被扫描的比例是小于等于
	 * 右侧最大值的比例。但只相信非默认估计，否则
	 * 维持我们的 1.0。
	 */
	fc_selec = fc_scalarineqsel(fc_root, fc_leop, fc_isgt, true, fc_collation, &fc_leftvar,
						  fc_rightmax, fc_op_righttype);
	if (fc_selec != DEFAULT_INEQ_SEL)
		*fc_leftend = fc_selec;

	/* 对右侧变量也是类似。 */
	fc_selec = fc_scalarineqsel(fc_root, fc_revleop, fc_isgt, true, fc_collation, &fc_rightvar,
						  fc_leftmax, fc_op_lefttype);
	if (fc_selec != DEFAULT_INEQ_SEL)
		*fc_rightend = fc_selec;

	/*
	 * 两个“结束”比例中只能有一个真实值小于 1.0；
	 * 相信较小的估计并将另一个重置为恰好 1.0。如果
	 * 我们得到完全相等的估计（这在自连接中很容易发生），
	 * 那么都不相信。
	 */
	if (*fc_leftend > *fc_rightend)
		*fc_leftend = 1.0;
	else if (*fc_leftend < *fc_rightend)
		*fc_rightend = 1.0;
	else
		*fc_leftend = *fc_rightend = 1.0;

	/*
	 * 此外，在找到第一个连接对之前，左侧变量
	 * 被扫描的比例是小于右侧最小值的比例。
	 * 但只相信非默认估计，否则维持我们自己的默认值。
	 */
	fc_selec = fc_scalarineqsel(fc_root, fc_ltop, fc_isgt, false, fc_collation, &fc_leftvar,
						  fc_rightmin, fc_op_righttype);
	if (fc_selec != DEFAULT_INEQ_SEL)
		*fc_leftstart = fc_selec;

	/* 对右侧变量也是类似。 */
	fc_selec = fc_scalarineqsel(fc_root, fc_revltop, fc_isgt, false, fc_collation, &fc_rightvar,
						  fc_leftmin, fc_op_lefttype);
	if (fc_selec != DEFAULT_INEQ_SEL)
		*fc_rightstart = fc_selec;

	/*
	 * 只有两个“开始”分数中的一个可以真正大于零；
	 * 相信较大的估计值，并将另一个重置为0.0。如果
	 * 我们得到了完全相等的估计（在自连接时很容易发生），
	 * 则不相信任何一个。
	 */
	if (*fc_leftstart < *fc_rightstart)
		*fc_leftstart = 0.0;
	else if (*fc_leftstart > *fc_rightstart)
		*fc_rightstart = 0.0;
	else
		*fc_leftstart = *fc_rightstart = 0.0;

	/*
	 * 如果排序顺序是null优先，我们将不得不跳过任何
	 * null。这些不会被scalarineqsel计数，我们
	 * 可以安全地将这一部分添加进来，无论我们是否相信
	 * scalarineqsel的结果。但一定要将总和限制为1.0！
	 */
	if (fc_nulls_first)
	{
		Form_pg_statistic fc_stats;

		if (HeapTupleIsValid(fc_leftvar.statsTuple))
		{
			fc_stats = (Form_pg_statistic) GETSTRUCT(fc_leftvar.statsTuple);
			*fc_leftstart += fc_stats->stanullfrac;
			CLAMP_PROBABILITY(*fc_leftstart);
			*fc_leftend += fc_stats->stanullfrac;
			CLAMP_PROBABILITY(*fc_leftend);
		}
		if (HeapTupleIsValid(fc_rightvar.statsTuple))
		{
			fc_stats = (Form_pg_statistic) GETSTRUCT(fc_rightvar.statsTuple);
			*fc_rightstart += fc_stats->stanullfrac;
			CLAMP_PROBABILITY(*fc_rightstart);
			*fc_rightend += fc_stats->stanullfrac;
			CLAMP_PROBABILITY(*fc_rightend);
		}
	}

	/* 不相信 start >= end，以防这种情况发生 */
	if (*fc_leftstart >= *fc_leftend)
	{
		*fc_leftstart = 0.0;
		*fc_leftend = 1.0;
	}
	if (*fc_rightstart >= *fc_rightend)
	{
		*fc_rightstart = 0.0;
		*fc_rightend = 1.0;
	}

fail:
	ReleaseVariableStats(fc_leftvar);
	ReleaseVariableStats(fc_rightvar);
}


/*
 *	matchingsel -- 通用匹配操作符选择性支持
 *
 * 对于任何 (a) 在我们收集标准统计信息的数据类型上的操作符，以及 
 * (b) 其默认估计（两倍DEFAULT_EQ_SEL）是合理的行为，使用这些。
 * 通常这对于类似匹配的操作符是有效的。
 */

Datum matchingsel(PG_FUNCTION_ARGS)
{
	PlannerInfo *fc_root = (PlannerInfo *) PG_GETARG_POINTER(0);
	Oid			fc_operator = PG_GETARG_OID(1);
	List	   *fc_args = (List *) PG_GETARG_POINTER(2);
	int			fc_varRelid = PG_GETARG_INT32(3);
	Oid			fc_collation = PG_GET_COLLATION();
	double		fc_selec;

	/* 使用通用限制选择性逻辑。 */
	fc_selec = generic_restriction_selectivity(fc_root, fc_operator, fc_collation,
											fc_args, fc_varRelid,
											DEFAULT_MATCHING_SEL);

	PG_RETURN_FLOAT8((float8) fc_selec);
}

Datum matchingjoinsel(PG_FUNCTION_ARGS)
{
	/* 目前就先搁置。 */
	PG_RETURN_FLOAT8(DEFAULT_MATCHING_SEL);
}


/*
 * estimate_num_groups 的辅助例程：将一个项目添加到
 * GroupVarInfos 列表中，但前提是它不像任何
 * 现有条目那样已知相等。
 */
typedef struct
{
	Node	   *var;			/* 可能是一个表达式，而不仅仅是一个 Var */
	RelOptInfo *rel;			/* 它所属的关系 */
	double		ndistinct;		/* # 不同的值 */
	bool		isdefault;		/* 如果使用了DEFAULT_NUM_DISTINCT，则为true */
} GroupVarInfo;

static List * add_unique_group_var(PlannerInfo *fc_root, List *fc_varinfos,
					 Node *fc_var, VariableStatData *fc_vardata)
{
	GroupVarInfo *fc_varinfo;
	double		fc_ndistinct;
	bool		fc_isdefault;
	ListCell   *fc_lc;

	fc_ndistinct = get_variable_numdistinct(fc_vardata, &fc_isdefault);

	foreach(fc_lc, fc_varinfos)
	{
		fc_varinfo = (GroupVarInfo *) lfirst(fc_lc);

		/* 删除确切的重复项 */
		if (equal(fc_var, fc_varinfo->var))
			return fc_varinfos;

		/*
		 * 删除已知相等的变量，但前提是它们属于不同的
		 * 关系（见 estimate_num_groups 的注释）
		 */
		if (fc_vardata->rel != fc_varinfo->rel &&
			exprs_known_equal(fc_root, fc_var, fc_varinfo->var))
		{
			if (fc_varinfo->ndistinct <= fc_ndistinct)
			{
				/* 保留较旧的项目，忘记新的 */
				return fc_varinfos;
			}
			else
			{
				/* 删除较旧的项目 */
				fc_varinfos = foreach_delete_current(fc_varinfos, fc_lc);
			}
		}
	}

	fc_varinfo = (GroupVarInfo *) palloc(sizeof(GroupVarInfo));

	fc_varinfo->var = fc_var;
	fc_varinfo->rel = fc_vardata->rel;
	fc_varinfo->ndistinct = fc_ndistinct;
	fc_varinfo->isdefault = fc_isdefault;
	fc_varinfos = lappend(fc_varinfos, fc_varinfo);
	return fc_varinfos;
}


/*
 * estimate_num_groups		- 估计分组查询中的组数
 *
 * 给定一个具有 GROUP BY 子句的查询，估计将会有多少组——即 GROUP BY 表达式的不同组合的数量。
 *
 * 该例程还用于估计 DISTINCT 过滤步骤所产生的行数；这是一个同构问题。 （注意：实际上，我们仅在 DISTINCT 前没有分组或聚合时使用它。）
 *
 * 输入：
 *	root - 查询
 *	groupExprs - 被分组的表达式列表
 *	input_rows - 预计到达分组/唯一过滤步骤的行数
 *	pgset - NULL, 或指向用于过滤 groupExprs 的分组集的 List**
 *
 * 输出：
 *	estinfo - 当作为非 NULL 传入时，该函数将设置 "flags" 字段中的位，以提供调用者关于估计的额外信息。目前，如果我们在估计中使用了任何默认值，则只设置 SELFLAG_USED_DEFAULT 位。
 *
 * 由于系统中缺乏任何交叉相关统计，因此对涉及多个 Var 的 GROUP BY 条件进行任何真正可靠的操作是不可能的。然而，我们应该避免假设最坏情况（所有可能的交叉乘积项实际上作为组出现），因为被分组的 Var 通常高度相关。我们目前的方法如下：
 *	1.  产生布尔值的表达式被假设为贡献两个组，与其内容无关，并在后续步骤中被忽略。这主要是因为像 "col IS NULL" 这样的测试在步骤 2 中破坏了所使用的启发式。
 *	2.  将给定的表达式简化为使用的唯一 Var 的列表。例如，GROUP BY a, a + b 被视为与 GROUP BY a, b 相同。显然，不计算相同的 Var 多于一次是正确的。将 f(x) 与 x 等同对待也是合理的：f() 不能增加不同值的数量（除非它是易失性的，而我们认为分组的可能性不大），但它也可能不会大幅减少不同值的数量。作为特例，如果一个 GROUP BY 表达式可以与我们有统计信息的表达式索引匹配，则我们将整个表达式视为仅是 Var。
 *	3.  如果列表包含因等价类而已知相等的不同关系的 Var，则从每个已知相等集丢弃所有但一个 Var，保留估计的值数量最小的那个（因为其他的额外值不能出现在连接行中）。注意，我们只考虑不同关系的 Var 的原因是，如果考虑相同关系的 Var，我们将在下一步中对等式的限制选择性进行重复计数。
 *	4.  对于单个源关系中的 Var，我们将值的数量相乘，限制在关系中的行数（如果有多个 Var 则除以 10），然后乘以基于该关系的限制子句的选择性调整因子。当有多个 Var 时，初始乘积可能太高（这是最坏情况），但限制在关系行的一个分数似乎是一种有帮助的启发式，以防止估计失控。 （10 的因子是根据预 Postgres-7.4 实践得出的。）我们乘以的因子以调整限制选择性假设限制子句与分组无关，这可能不是一个有效的假设，但很难做得更好。
 *	5.  如果有来自多个关系的 Var，我们对每个此类关系重复步骤 4，并将结果相乘。
 * 注意，不包含被分组的 Var 的关系完全被忽略，连接子句也同样如此。这些关系无法增加组的数量，我们假设这些子句也不会减少数量（有些不太合理，但我们没有信息做得更好）。
 */
double estimate_num_groups(PlannerInfo *fc_root, List *fc_groupExprs, double fc_input_rows,
					List **fc_pgset, EstimationInfo *fc_estinfo)
{
	List	   *fc_varinfos = NIL;
	double		fc_srf_multiplier = 1.0;
	double		fc_numdistinct;
	ListCell   *fc_l;
	int			fc_i;

	/* 如果 estinfo 输出参数非 NULL，则将其置零 */
	if (fc_estinfo != NULL)
		memset(fc_estinfo, 0, sizeof(EstimationInfo));

	/*
	 * 我们绝对不希望返回零组的估计，因为这往往会导致除零和其他不愉快。输入行的估计通常至少为 1，但为了以防万一，将其限制为 1。
	 */
	fc_input_rows = clamp_row_est(fc_input_rows);

	/*
	 * 如果没有分组列，则只有一个组。 （这在正常情况下的 GROUP BY 或 DISTINCT 中不可能发生，但在带集合操作的极端情况中是可能的。）
	 */
	if (fc_groupExprs == NIL || (fc_pgset && list_length(*fc_pgset) < 1))
		return 1.0;

	/*
	 * 计算来自布尔分组表达式的组数。对于其他表达式，寻找使用的唯一 Var，如果我们可以找到其统计信息，则将表达式视为 Var。对于每一个，记录不同值数量的统计估计（其表中的总数，不考虑过滤）。
	 */
	fc_numdistinct = 1.0;

	fc_i = 0;
	foreach(fc_l, fc_groupExprs)
	{
		Node	   *fc_groupexpr = (Node *) lfirst(fc_l);
		double		fc_this_srf_multiplier;
		VariableStatData fc_vardata;
		List	   *fc_varshere;
		ListCell   *fc_l2;

		/* 此表达式在此分组集中吗？ */
		if (fc_pgset && !list_member_int(*fc_pgset, fc_i++))
			continue;

		
/*
		 * 在分组列中，返回集合的函数有点问题。
		 * 下面的代码将有效地忽略它们的SRF特性，并得出一个
		 * 像标量函数一样的numdistinct估计。
		 * 我们通过按最大的SRF
		 * 行计数估计来缩放最终结果来进行补偿。 （如果SRF
		 * 生成任何输出值的多个副本，这将是一个过高的估计，但假设SRF的输出是独特的似乎最合适。
		 * 在任何情况下，没有比我们今天拥有的SRF输出行计数的更好估计，
		 * 可能没有必要过于担心这个问题。）
		 */
		fc_this_srf_multiplier = expression_returns_set_rows(fc_root, fc_groupexpr);
		if (fc_srf_multiplier < fc_this_srf_multiplier)
			fc_srf_multiplier = fc_this_srf_multiplier;

		/* 针对返回布尔值的表达式的短路处理 */
		if (exprType(fc_groupexpr) == BOOLOID)
		{
			fc_numdistinct *= 2.0;
			continue;
		}

		/*
		 * 如果examine_variable能够推断出关于GROUP BY
		 * 表达式的任何信息，即使它实际上更复杂，也将其视为单个变量。
		 *
		 * XXX 这意味着如果表达式上有统计对象，我们不会将其拆分为单独的Vars。
		 * 这影响我们在
		 * estimate_multivariate_ndistinct中的统计选择，因为使用每个表达式的更准确估计并将其视为独立可能更好，而不是对提取变量的估计进行组合，
		 * 我们不知道这与表达式的关系。
		 */
		examine_variable(fc_root, fc_groupexpr, 0, &fc_vardata);
		if (HeapTupleIsValid(fc_vardata.statsTuple) || fc_vardata.isunique)
		{
			fc_varinfos = add_unique_group_var(fc_root, fc_varinfos,
											fc_groupexpr, &fc_vardata);
			ReleaseVariableStats(fc_vardata);
			continue;
		}
		ReleaseVariableStats(fc_vardata);

		/*
		 * 否则提取组件Vars。处理PlaceHolderVars，通过递归它们的参数来处理
		 * （实际假设PlaceHolderVar不改变组的数量，这归结为忽略可能向结果集添加的空值）。
		 */
		fc_varshere = pull_var_clause(fc_groupexpr,
								   PVC_RECURSE_AGGREGATES |
								   PVC_RECURSE_WINDOWFUNCS |
								   PVC_RECURSE_PLACEHOLDERS);

		/*
		 * 如果发现任何无变量的GROUP BY项，那么它要么是常量（我们可以忽略它），
		 * 要么包含一个易变函数；在后者的情况下，我们选择放弃，并假设每个输入行将
		 * 产生一个独特的组。
		 */
		if (fc_varshere == NIL)
		{
			if (contain_volatile_functions(fc_groupexpr))
				return fc_input_rows;
			continue;
		}

		/*
		 * 否则将变量添加到varinfos列表中
		 */
		foreach(fc_l2, fc_varshere)
		{
			Node	   *fc_var = (Node *) lfirst(fc_l2);

			examine_variable(fc_root, fc_var, 0, &fc_vardata);
			fc_varinfos = add_unique_group_var(fc_root, fc_varinfos, fc_var, &fc_vardata);
			ReleaseVariableStats(fc_vardata);
		}
	}

	/*
	 * 如果现在没有Vars，我们必须拥有一个全常量或全布尔的GROUP BY
	 * 列表。
	 */
	if (fc_varinfos == NIL)
	{
		/* 按照长路径中使用的方式应用SRF乘数 */
		fc_numdistinct *= fc_srf_multiplier;
		/* 四舍五入 */
		fc_numdistinct = ceil(fc_numdistinct);
		/* 防止超出范围的答案 */
		if (fc_numdistinct > fc_input_rows)
			fc_numdistinct = fc_input_rows;
		if (fc_numdistinct < 1.0)
			fc_numdistinct = 1.0;
		return fc_numdistinct;
	}

	/*
	 * 按关系对变量进行分组，并估计总的不同数量。
	 *
	 * 对于外循环的每次迭代，我们处理最前面的变量在
	 * varinfos中，以及同一关系中的所有其他变量。我们将这些
	 * 变量从新变量信息列表中移除，以便继续进行下一次迭代。这是
	 * 将相同关系的变量分组的最简单方法。
	 */
	do
	{
		GroupVarInfo *fc_varinfo1 = (GroupVarInfo *) linitial(fc_varinfos);
		RelOptInfo *fc_rel = fc_varinfo1->rel;
		double		fc_reldistinct = 1;
		double		fc_relmaxndistinct = fc_reldistinct;
		int			fc_relvarcount = 0;
		List	   *fc_newvarinfos = NIL;
		List	   *fc_relvarinfos = NIL;

		/*
		 * 将varinfos列表分为两部分 - 一部分为当前关系，另一部分
		 * 为其他关系中剩余的变量。
		 */
		fc_relvarinfos = lappend(fc_relvarinfos, fc_varinfo1);
		for_each_from(fc_l, fc_varinfos, 1)
		{
			GroupVarInfo *fc_varinfo2 = (GroupVarInfo *) lfirst(fc_l);

			if (fc_varinfo2->rel == fc_varinfo1->rel)
			{
				/* 当前关系的变量信息 */
				fc_relvarinfos = lappend(fc_relvarinfos, fc_varinfo2);
			}
			else
			{
				/* 还没有时间处理varinfo2 */
				fc_newvarinfos = lappend(fc_newvarinfos, fc_varinfo2);
			}
		}

		/*
		 * 获取此关系的变量的不同数量估计。我们
		 * 迭代搜索多变量n-distinct，具有最大数量的
		 * 变量；假设每个变量组是独立于其他变量组的，
		 * 我们将它们相乘。任何在没有找到更多
		 * 多变量匹配后的剩余关系变量信息也被视为独立的，
		 * 因此它们的个别ndistinct估计也会相乘。
		 *
		 * 在迭代过程中，统计我们应用了多少不同的
		 * numdistinct值。我们在下面应用一个修正因子，
		 * 但仅在我们乘以多个此类值时。
		 */
		while (fc_relvarinfos)
		{
			double		fc_mvndistinct;

			if (fc_estimate_multivariate_ndistinct(fc_root, fc_rel, &fc_relvarinfos,
												&fc_mvndistinct))
			{
				fc_reldistinct *= fc_mvndistinct;
				if (fc_relmaxndistinct < fc_mvndistinct)
					fc_relmaxndistinct = fc_mvndistinct;
				fc_relvarcount++;
			}
			else
			{
				foreach(fc_l, fc_relvarinfos)
				{
					GroupVarInfo *fc_varinfo2 = (GroupVarInfo *) lfirst(fc_l);

					fc_reldistinct *= fc_varinfo2->ndistinct;
					if (fc_relmaxndistinct < fc_varinfo2->ndistinct)
						fc_relmaxndistinct = fc_varinfo2->ndistinct;
					fc_relvarcount++;

					/*
					 * 当varinfo2的isdefault被设置时，我们最好在
					 * EstimationInfo中设置SELFLAG_USED_DEFAULT位。
					 */
					if (fc_estinfo != NULL && fc_varinfo2->isdefault)
						fc_estinfo->flags |= SELFLAG_USED_DEFAULT;
				}

				/* 我们完成了这个关系 */
				fc_relvarinfos = NIL;
			}
		}

		/*
		 * 健全性检查 --- 如果关系为空则不要除以零。
		 */
		Assert(IS_SIMPLE_REL(fc_rel));
		if (fc_rel->tuples > 0)
		{
			/*
			 * 限制到关系的大小，或当有多个变量时限制到关系的大小 / 10。
			 * 修正因子是因为这些变量可能有关联，但我们
			 * 不知道有多少。然而，我们绝不应限制到
			 * 任何变量的最大ndistinct值以下，因为一定会
			 * 至少有那么多组。
			 */
			double		fc_clamp = fc_rel->tuples;

			if (fc_relvarcount > 1)
			{
				fc_clamp *= 0.1;
				if (fc_clamp < fc_relmaxndistinct)
				{
					fc_clamp = fc_relmaxndistinct;
					/* 为了健全起见，以防某些ndistinct过大： */
					if (fc_clamp > fc_rel->tuples)
						fc_clamp = fc_rel->tuples;
				}
			}
			if (fc_reldistinct > fc_clamp)
				fc_reldistinct = fc_clamp;

			/*
			 * 根据限制选择性更新估计，
			 * 防止在reldistinct为零时除以零。
			 * 如果我们知道要返回所有行，也跳过此步骤。
			 */
			if (fc_reldistinct > 0 && fc_rel->rows < fc_rel->tuples)
			{
				/*
				 * 假设一个表包含N行，在均匀分布中有n个不同的值，
				 * 如果我们随机选择p行，则预期选择的不同值的数量为
				 *
				 * n * (1 - product((N-N/n-i)/(N-i), i=0..p-1))
				 *
				 * = n * (1 - (N-N/n)! / (N-N/n-p)! * (N-p)! / N!)
				 *
				 * 见“在数据库组织中近似块访问”，
				 * S. B. Yao，ACM通讯，卷20，第4期，1977年4月，第260-261页。
				 *
				 * 或者，重新排列阶乘的项，可以写作
				 *
				 * n * (1 - product((N-p-i)/(N-i), i=0..N/n-1))
				 *
				 * 这个公式的形式在p大于N/n的常见情况下计算上更高效。此外，
				 * 正如Dell'Era所指出的，如果对于乘积中的所有项i << N，
				 * 它可以被近似为
				 *
				 * n * (1 - ((N-p)/N)^(N/n))
				 *
				 * 见“从不带替换的袋子中选择时的期望不同值”，
				 * Alberto Dell'Era，http://www.adellera.it/investigations/distinct_balls/。
				 *
				 * 条件i << N等价于n >> 1，因此当表中不同值的数量较大时，
				 * 这是一个良好的近似。事实证明，这个公式即使在n较小的时候
				 * 也能很好地发挥作用。
				 */
				fc_reldistinct *=
					(1 - pow((fc_rel->tuples - fc_rel->rows) / fc_rel->tuples,
							 fc_rel->tuples / fc_reldistinct));
			}
			fc_reldistinct = clamp_row_est(fc_reldistinct);

			/*
			 * 更新总不同组的估计。
			 */
			fc_numdistinct *= fc_reldistinct;
		}

		fc_varinfos = fc_newvarinfos;
	} while (fc_varinfos != NIL);

	/* 现在我们可以考虑任何SRF的影响 */
	fc_numdistinct *= fc_srf_multiplier;

	/* 四舍五入 */
	fc_numdistinct = ceil(fc_numdistinct);

	/* 防止超出范围的答案 */
	if (fc_numdistinct > fc_input_rows)
		fc_numdistinct = fc_input_rows;
	if (fc_numdistinct < 1.0)
		fc_numdistinct = 1.0;

	return fc_numdistinct;
}


/*
 * 估算在给定数量的桶中使用指定表达式作为哈希键时的哈希桶统计。
 *
 * 这尝试确定两个值：
 *
 * 1. 表达式最常见值的频率（如果无法获取，则返回零到 *mcv_freq）。
 *
 * 2. "桶大小比例"，即每个桶中的平均条目数除以关系中的总元组数。
 *
 * XXX 这实际上相当虚假，因为我们有效地假设在应用限制子句后哈希键的分布将与底层关系中的分布相同。然而，我们并没有足够的聪明才智去弄清楚限制子句如何改变分布，因此这暂时可以接受。
 *
 * 我们传递给执行器的桶数量是为给定的输入关系使用的。如果数据完全分布均匀，每个可用桶中有相同数量的元组，则桶大小比例将为 1/nbuckets。 但这种快乐的状态只会发生在 (a) 至少有 nbuckets 个不同的数据值，并且 (b) 数据分布不是太倾斜的情况下。否则，桶将会不均匀占据。如果连接中的另一个关系的键分布与这个关系的相似，那么最满的桶就是那些最常被探测的桶。因此，出于成本考虑，"平均"桶大小实际上应该接近于"最坏情况"桶大小。我们试图通过调整比例（如果不同数据值太少）来估算这一点，然后根据最常见值的频率与平均频率的比例进行放大。
 *
 * 如果没有可用的统计信息，则使用默认估计 0.1。如果内部关系很大，这将非常强烈地劝阻使用哈希，这正是我们想要的。除非我们知道内部关系是良好分布的（否则替代方案似乎更糟），我们不想哈希。
 *
 * 调用者还应该检查 mcv_freq 是否不大到使最常见值本身需要一个不切实际的大桶。在哈希连接中，执行器可以在桶变得太大的时候拆分桶，但显然这对包含许多重复相同值的桶并没有帮助。
 */
void estimate_hash_bucket_stats(PlannerInfo *fc_root, Node *fc_hashkey, double fc_nbuckets,
						   Selectivity *fc_mcv_freq,
						   Selectivity *fc_bucketsize_frac)
{
	VariableStatData fc_vardata;
	double		fc_estfract,
				fc_ndistinct,
				fc_stanullfrac,
				fc_avgfreq;
	bool		fc_isdefault;
	AttStatsSlot fc_sslot;

	examine_variable(fc_root, fc_hashkey, 0, &fc_vardata);

	/* 查找最常见值的频率（如果可用） */
	*fc_mcv_freq = 0.0;

	if (HeapTupleIsValid(fc_vardata.statsTuple))
	{
		if (get_attstatsslot(&fc_sslot, fc_vardata.statsTuple,
							 STATISTIC_KIND_MCV, InvalidOid,
							 ATTSTATSSLOT_NUMBERS))
		{
			/*
			 * 第一个 MCV 统计是最常见值的。
			 */
			if (fc_sslot.nnumbers > 0)
				*fc_mcv_freq = fc_sslot.numbers[0];
			free_attstatsslot(&fc_sslot);
		}
	}

	/* 获取不同值的数量 */
	fc_ndistinct = get_variable_numdistinct(&fc_vardata, &fc_isdefault);

	/*
	 * 如果 ndistinct 不是实际值，则返回。我们通常返回 0.1，但如果
	 * mcv_freq 被知道高于这个值，则使用它。
	 */
	if (fc_isdefault)
	{
		*fc_bucketsize_frac = (Selectivity) Max(0.1, *fc_mcv_freq);
		ReleaseVariableStats(fc_vardata);
		return;
	}

	/* 获取空值的比例 */
	if (HeapTupleIsValid(fc_vardata.statsTuple))
	{
		Form_pg_statistic fc_stats;

		fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata.statsTuple);
		fc_stanullfrac = fc_stats->stanullfrac;
	}
	else
		fc_stanullfrac = 0.0;

	/* 计算原始关系中所有不同数据值的平均频率 */
	fc_avgfreq = (1.0 - fc_stanullfrac) / fc_ndistinct;

	/*
	 * 调整 ndistinct 以考虑限制子句。注意，我们假设数据分布
	 * 受到限制子句的均匀影响！
	 *
	 * XXX 可能有更好的方法，但代价更高：乘以提到目标 Var 的
	 * 关系限制子句的选择性。
	 */
	if (fc_vardata.rel && fc_vardata.rel->tuples > 0)
	{
		fc_ndistinct *= fc_vardata.rel->rows / fc_vardata.rel->tuples;
		fc_ndistinct = clamp_row_est(fc_ndistinct);
	}

	/*
	 * 如果桶数少于预期的不同值的数量，则初步估计桶大小比例为 1/nbuckets;
	 * 否则为 1/ndistinct。
	 */
	if (fc_ndistinct > fc_nbuckets)
		fc_estfract = 1.0 / fc_nbuckets;
	else
		fc_estfract = 1.0 / fc_ndistinct;

	/*
	 * 向上调整估计的桶大小，以考虑倾斜的分布。
	 */
	if (fc_avgfreq > 0.0 && *fc_mcv_freq > fc_avgfreq)
		fc_estfract *= *fc_mcv_freq / fc_avgfreq;

	/*
	 * 将桶大小限制在合理范围内（上述调整可能很容易
	 * 产生超出范围的结果）。我们将下限设置为略高于零，因为零不是
	 * 非常合理的结果。
	 */
	if (fc_estfract < 1.0e-6)
		fc_estfract = 1.0e-6;
	else if (fc_estfract > 1.0)
		fc_estfract = 1.0;

	*fc_bucketsize_frac = (Selectivity) fc_estfract;

	ReleaseVariableStats(fc_vardata);
}

/*
 * estimate_hashagg_tablesize
 *	  估算哈希聚合哈希表所需的字节数，基于 agg_costs、路径宽度和组数。
 *
 * 我们将结果返回为 "double" 以防止在与 dNumGroups 相乘时可能出现的溢出问题。
 *
 * XXX 现在哈希聚合知道省略不必要的列在哈希表中，可能会高估大小。
 * 同样，对于混合模式分组集，未在哈希集合中的分组列也在这里计数，
 * 尽管哈希聚合不会存储它们。这是个问题吗？
 */
double estimate_hashagg_tablesize(PlannerInfo *fc_root, Path *fc_path,
						   const AggClauseCosts *fc_agg_costs, double fc_dNumGroups)
{
	Size		fc_hashentrysize;

	fc_hashentrysize = hash_agg_entry_size(list_length(fc_root->aggtransinfos),
										fc_path->pathtarget->width,
										fc_agg_costs->transitionSpace);

	/*
	 * 请注意，这忽略了哈希表的填充因子和增长策略的影响。 
	 * 这可能是可以的，因为默认填充因子相对较高。在
	 * 此处合理地考虑“增长一倍”的增长策略会很困难。
	 */
	return fc_hashentrysize * fc_dNumGroups;
}


/*-------------------------------------------------------------------------
 *
 * 支持例程
 *
 *-------------------------------------------------------------------------
 */


static bool fc_estimate_multivariate_ndistinct(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								List **fc_varinfos, double *fc_ndistinct)
{
	ListCell   *fc_lc;
	int			fc_nmatches_vars;
	int			fc_nmatches_exprs;
	Oid			fc_statOid = InvalidOid;
	MVNDistinct *fc_stats;
	StatisticExtInfo *fc_matched_info = NULL;
	RangeTblEntry *fc_rte = planner_rt_fetch(fc_rel->relid, fc_root);

	/* 如果表没有扩展统计信息，立即退出 */
	if (!fc_rel->statlist)
		return false;

	/* 查找与变量数量最多的 ndistinct 统计对象 */
	fc_nmatches_vars = 0;			/* 我们至少需要两个匹配项 */
	fc_nmatches_exprs = 0;
	foreach(fc_lc, fc_rel->statlist)
	{
		ListCell   *fc_lc2;
		StatisticExtInfo *fc_info = (StatisticExtInfo *) lfirst(fc_lc);
		int			fc_nshared_vars = 0;
		int			fc_nshared_exprs = 0;

		/* 跳过其他类型的统计信息 */
		if (fc_info->kind != STATS_EXT_NDISTINCT)
			continue;

		/* 跳过具有不匹配 stxdinherit 值的统计信息 */
		if (fc_info->inherit != fc_rte->inh)
			continue;

		/*
		 * 确定有多少表达式（以及未匹配表达式中的变量）匹配。然后我们将使用这些数字来选择与子句最匹配的统计对象。
		 */
		foreach(fc_lc2, *fc_varinfos)
		{
			ListCell   *fc_lc3;
			GroupVarInfo *fc_varinfo = (GroupVarInfo *) lfirst(fc_lc2);
			AttrNumber	fc_attnum;

			Assert(fc_varinfo->rel == fc_rel);

			/* 简单的 Var，直接在统计键中搜索 */
			if (IsA(fc_varinfo->var, Var))
			{
				fc_attnum = ((Var *) fc_varinfo->var)->varattno;

				/*
				 * 忽略系统属性 - 我们不支持其上的统计信息，因此无法与之匹配（并且会因为值为负而失败）。
				 */
				if (!AttrNumberIsForUserDefinedAttr(fc_attnum))
					continue;

				if (bms_is_member(fc_attnum, fc_info->keys))
					fc_nshared_vars++;

				continue;
			}

			/* 表达式 - 查看它是否在统计对象中 */
			foreach(fc_lc3, fc_info->exprs)
			{
				Node	   *fc_expr = (Node *) lfirst(fc_lc3);

				if (equal(fc_varinfo->var, fc_expr))
				{
					fc_nshared_exprs++;
					break;
				}
			}
		}

		if (fc_nshared_vars + fc_nshared_exprs < 2)
			continue;

		/*
		 * 该统计对象是否匹配的列数比当前最优对象更多？ 如果是，则改用此对象。
		 *
		 * XXX 这应该使用对象名称或类似的东西来打破平局，以使结果稳定。
		 */
		if ((fc_nshared_exprs > fc_nmatches_exprs) ||
			(((fc_nshared_exprs == fc_nmatches_exprs)) && (fc_nshared_vars > fc_nmatches_vars)))
		{
			fc_statOid = fc_info->statOid;
			fc_nmatches_vars = fc_nshared_vars;
			fc_nmatches_exprs = fc_nshared_exprs;
			fc_matched_info = fc_info;
		}
	}

	/* 没有匹配？ */
	if (fc_statOid == InvalidOid)
		return false;

	Assert(fc_nmatches_vars + fc_nmatches_exprs > 1);

	fc_stats = statext_ndistinct_load(fc_statOid, fc_rte->inh);

	/*
	 * 如果我们有匹配项，请在其中搜索匹配的特定项（必须有一个），并构建输出值。
	 */
	if (fc_stats)
	{
		int			fc_i;
		List	   *fc_newlist = NIL;
		MVNDistinctItem *fc_item = NULL;
		ListCell   *fc_lc2;
		Bitmapset  *fc_matched = NULL;
		AttrNumber	fc_attnum_offset;

		/*
		 * 我们需要偏移多少 attnums？ 如果没有表达式，则不需要偏移。否则偏移足够以将最低的一个（等于表达式数量）移动到 1。
		 */
		if (fc_matched_info->exprs)
			fc_attnum_offset = (list_length(fc_matched_info->exprs) + 1);
		else
			fc_attnum_offset = 0;

		/* 查看实际匹配的内容 */
		foreach(fc_lc2, *fc_varinfos)
		{
			ListCell   *fc_lc3;
			int			fc_idx;
			bool		fc_found = false;

			GroupVarInfo *fc_varinfo = (GroupVarInfo *) lfirst(fc_lc2);

			/*
			 * 处理简单 Var 表达式，直接与键匹配。如果有匹配的表达式，我们将稍后尝试匹配它。
			 */
			if (IsA(fc_varinfo->var, Var))
			{
				AttrNumber	fc_attnum = ((Var *) fc_varinfo->var)->varattno;

				/*
				 * 忽略系统属性上的表达式。无法依赖负值的 bms 检查。
				 */
				if (!AttrNumberIsForUserDefinedAttr(fc_attnum))
					continue;

				/* 变量是否被统计对象覆盖？ */
				if (!bms_is_member(fc_attnum, fc_matched_info->keys))
					continue;

				fc_attnum = fc_attnum + fc_attnum_offset;

				/* 确保有足够的偏移量 */
				Assert(AttrNumberIsForUserDefinedAttr(fc_attnum));

				fc_matched = bms_add_member(fc_matched, fc_attnum);

				fc_found = true;
			}

			/*
			 * XXX 也许我们应该允许在找到匹配表达式的属性后继续搜索表达式？这将处理像 "(a)" 这样的简单表达式，但看起来相当无用。
			 */
			if (fc_found)
				continue;

			/* 表达式 - 查看它是否在统计对象中 */
			fc_idx = 0;
			foreach(fc_lc3, fc_matched_info->exprs)
			{
				Node	   *fc_expr = (Node *) lfirst(fc_lc3);

				if (equal(fc_varinfo->var, fc_expr))
				{
					AttrNumber	fc_attnum = -(fc_idx + 1);

					fc_attnum = fc_attnum + fc_attnum_offset;

					/* 确保有足够的偏移量 */
					Assert(AttrNumberIsForUserDefinedAttr(fc_attnum));

					fc_matched = bms_add_member(fc_matched, fc_attnum);

					/* 应该只有一个匹配的表达式 */
					break;
				}

				fc_idx++;
			}
		}

		/* 查找与组合完全匹配的特定项 */
		for (fc_i = 0; fc_i < fc_stats->nitems; fc_i++)
		{
			int			fc_j;
			MVNDistinctItem *fc_tmpitem = &fc_stats->items[fc_i];

			if (fc_tmpitem->nattributes != bms_num_members(fc_matched))
				continue;

			/* 假设这是正确的项 */
			fc_item = fc_tmpitem;

			/* 检查所有项的属性/表达式是否符合匹配 */
			for (fc_j = 0; fc_j < fc_tmpitem->nattributes; fc_j++)
			{
				AttrNumber	fc_attnum = fc_tmpitem->attributes[fc_j];

				/*
				 * 感谢我们在上面构造的匹配位图，我们可以以相同的方式偏移所有 attnums。
				 */
				fc_attnum = fc_attnum + fc_attnum_offset;

				if (!bms_is_member(fc_attnum, fc_matched))
				{
					/* 不，是这个项 */
					fc_item = NULL;
					break;
				}
			}

			/*
			 * 如果该项具有所有匹配的属性，我们知道这是正确的 - 不可能有更好的匹配。
			 */
			if (fc_item)
				break;
		}

		/*
		 * 确保我们找到了一个项。必须有一个，因为 ndistinct 统计信息包含所有属性的组合。
		 */
		if (!fc_item)
			elog(ERROR, "corrupt MVNDistinct entry");

		/* 形成输出 varinfo 列表，只保留未匹配的项 */
		foreach(fc_lc, *fc_varinfos)
		{
			GroupVarInfo *fc_varinfo = (GroupVarInfo *) lfirst(fc_lc);
			ListCell   *fc_lc3;
			bool		fc_found = false;

			/*
			 * 让我们先看看普通变量，因为这是最常见的情况，并且检查相当便宜。我们可以简单地获取 attnum 并检查（带偏移）匹配位图。
			 */
			if (IsA(fc_varinfo->var, Var))
			{
				AttrNumber	fc_attnum = ((Var *) fc_varinfo->var)->varattno;

				/*
				 * 如果它是系统属性，我们就可以完成了。我们不支持系统属性上的扩展统计信息，因此显然没有匹配。只需保留表达式并继续。
				 */
				if (!AttrNumberIsForUserDefinedAttr(fc_attnum))
				{
					fc_newlist = lappend(fc_newlist, fc_varinfo);
					continue;
				}

				/* 应用与上面相同的偏移量 */
				fc_attnum += fc_attnum_offset;

				/* 如果没有匹配，保持 varinfo */
				if (!bms_is_member(fc_attnum, fc_matched))
					fc_newlist = lappend(fc_newlist, fc_varinfo);

				/* 循环的其余部分处理复杂表达式。 */
				continue;
			}

			/*
			 * 处理复杂表达式，而不仅仅是简单的 Vars。
			 *
			 * 首先，我们搜索表达式的精确匹配。如果找到匹配，
			 * 我们可以直接丢弃整个 GroupExprInfo，连同我们从中提取的所有
			 * 变量。
			 *
			 * 否则，我们检查单个变量，并尝试将其匹配到项中的变量。
			 */
			foreach(fc_lc3, fc_matched_info->exprs)
			{
				Node	   *fc_expr = (Node *) lfirst(fc_lc3);

				if (equal(fc_varinfo->var, fc_expr))
				{
					fc_found = true;
					break;
				}
			}

			/* 找到精确匹配，跳过 */
			if (fc_found)
				continue;

			fc_newlist = lappend(fc_newlist, fc_varinfo);
		}

		*fc_varinfos = fc_newlist;
		*fc_ndistinct = fc_item->ndistinct;
		return true;
	}

	return false;
}

/*
 * convert_to_scalar
 *	  将指示类型的非 NULL 值转换为 scalarineqsel() 所需的
 *	  比较尺度。
 *	  成功时返回“true”。
 *
 * XXX 这个例程是一个小技巧：理想情况下，我们应该在 pg_type 中查找
 * 转换子例程。
 *
 * 所有数值数据类型都简单地转换为它们等效的“double”值。
 * （超出“double”范围的 NUMERIC 值被限制在 +/- HUGE_VAL）
 *
 * 字符串数据类型通过 convert_string_to_scalar() 转换，
 * 其下文有解释。这个例程处理三种值，而不仅仅是一种，原因在于
 * 我们需要处理字符串。
 *
 * bytea 数据类型与字符串有所不同，因此必须单独处理。
 *
 * 代表绝对时间的几种数据类型都被转换为 Timestamp，
 * 实际上是一个 int64，然后我们将其提升为 double。
 * 注意，即使对于 Timestamp 的“特殊”值，这也会给出正确的结果，
 * 因为它们被选择以正确比较；请参见 timestamp_cmp。
 *
 * 代表相对时间（时间间隔）的几种数据类型都被转换为
 * 用秒表示的度量。
 */
static bool fc_convert_to_scalar(Datum fc_value, Oid fc_valuetypid, Oid fc_collid, double *fc_scaledvalue,
				  Datum fc_lobound, Datum fc_hibound, Oid fc_boundstypid,
				  double *fc_scaledlobound, double *fc_scaledhibound)
{
	bool		fc_failure = false;

	/*
	 * valuetypid 和 boundstypid 应该与我们调用的运算符的
	 * 声明输入类型完全匹配。然而，扩展可能会尝试使用 scalarineqsel
	 * 作为我们在这里处理的输入类型的运算符的估计器；
	 * 在这种情况下，我们希望返回 false，而不是失败。
	 * 无论如何，我们不能假定 valuetypid 和 boundstypid 是相同的。
	 *
	 * XXX 我们正在插值的直方图可能属于与声明类型仅
	 * 具有二进制兼容性的列。实质上，我们假设二进制兼容类型的行为
	 * 足够相似，以便我们可以使用一种类型的运算符生成的
	 * 直方图来估计另一种类型的选择性。
	 * 在某些情况下这显然是错误的，特别是符号与无符号的解释
	 * 可能会让我们陷入困境。但在大多数情况下这是足够有用的，
	 * 所以我们还是这样做。应该考虑更严格的方法来实现这一点。
	 */
	switch (fc_valuetypid)
	{
			/*
			 * 内置数值类型
			 */
		case BOOLOID:
		case INT2OID:
		case INT4OID:
		case INT8OID:
		case FLOAT4OID:
		case FLOAT8OID:
		case NUMERICOID:
		case OIDOID:
		case REGPROCOID:
		case REGPROCEDUREOID:
		case REGOPEROID:
		case REGOPERATOROID:
		case REGCLASSOID:
		case REGTYPEOID:
		case REGCOLLATIONOID:
		case REGCONFIGOID:
		case REGDICTIONARYOID:
		case REGROLEOID:
		case REGNAMESPACEOID:
			*fc_scaledvalue = fc_convert_numeric_to_scalar(fc_value, fc_valuetypid,
													 &fc_failure);
			*fc_scaledlobound = fc_convert_numeric_to_scalar(fc_lobound, fc_boundstypid,
													   &fc_failure);
			*fc_scaledhibound = fc_convert_numeric_to_scalar(fc_hibound, fc_boundstypid,
													   &fc_failure);
			return !fc_failure;

			/*
			 * 内置字符串类型
			 */
		case CHAROID:
		case BPCHAROID:
		case VARCHAROID:
		case TEXTOID:
		case NAMEOID:
			{
				char	   *fc_valstr = fc_convert_string_datum(fc_value, fc_valuetypid,
														  fc_collid, &fc_failure);
				char	   *fc_lostr = fc_convert_string_datum(fc_lobound, fc_boundstypid,
														 fc_collid, &fc_failure);
				char	   *fc_histr = fc_convert_string_datum(fc_hibound, fc_boundstypid,
														 fc_collid, &fc_failure);

				/*
				 * 如果任何值不是字符串类型，则退出。
				 * 我们可能会泄露其他值的转换字符串，但
				 * 这不值得担心。
				 */
				if (fc_failure)
					return false;

				fc_convert_string_to_scalar(fc_valstr, fc_scaledvalue,
										 fc_lostr, fc_scaledlobound,
										 fc_histr, fc_scaledhibound);
				pfree(fc_valstr);
				pfree(fc_lostr);
				pfree(fc_histr);
				return true;
			}

			/*
			 * 内置 bytea 类型
			 */
		case BYTEAOID:
			{
				/* 我们只支持 bytea 与 bytea 的比较 */
				if (fc_boundstypid != BYTEAOID)
					return false;
				fc_convert_bytea_to_scalar(fc_value, fc_scaledvalue,
										fc_lobound, fc_scaledlobound,
										fc_hibound, fc_scaledhibound);
				return true;
			}

			/*
			 * 内置时间类型
			 */
		case TIMESTAMPOID:
		case TIMESTAMPTZOID:
		case DATEOID:
		case INTERVALOID:
		case TIMEOID:
		case TIMETZOID:
			*fc_scaledvalue = fc_convert_timevalue_to_scalar(fc_value, fc_valuetypid,
													   &fc_failure);
			*fc_scaledlobound = fc_convert_timevalue_to_scalar(fc_lobound, fc_boundstypid,
														 &fc_failure);
			*fc_scaledhibound = fc_convert_timevalue_to_scalar(fc_hibound, fc_boundstypid,
														 &fc_failure);
			return !fc_failure;

			/*
			 * 内置网络类型
			 */
		case INETOID:
		case CIDROID:
		case MACADDROID:
		case MACADDR8OID:
			*fc_scaledvalue = convert_network_to_scalar(fc_value, fc_valuetypid,
													 &fc_failure);
			*fc_scaledlobound = convert_network_to_scalar(fc_lobound, fc_boundstypid,
													   &fc_failure);
			*fc_scaledhibound = convert_network_to_scalar(fc_hibound, fc_boundstypid,
													   &fc_failure);
			return !fc_failure;
	}
	/* 不知道如何转换 */
	*fc_scaledvalue = *fc_scaledlobound = *fc_scaledhibound = 0;
	return false;
}

/*
 * 对于任何数值数据类型执行 convert_to_scalar() 的工作。
 *
 * 失败时（例如，不支持的 typid），将 *failure 设置为 true；
 * 否则，该变量不变。
 */
static double fc_convert_numeric_to_scalar(Datum fc_value, Oid fc_typid, bool *fc_failure)
{
	switch (fc_typid)
	{
		case BOOLOID:
			return (double) DatumGetBool(fc_value);
		case INT2OID:
			return (double) DatumGetInt16(fc_value);
		case INT4OID:
			return (double) DatumGetInt32(fc_value);
		case INT8OID:
			return (double) DatumGetInt64(fc_value);
		case FLOAT4OID:
			return (double) DatumGetFloat4(fc_value);
		case FLOAT8OID:
			return (double) DatumGetFloat8(fc_value);
		case NUMERICOID:
			/* 注意：超出范围的值将被限制在 +-HUGE_VAL */
			return (double)
				DatumGetFloat8(DirectFunctionCall1(numeric_float8_no_overflow,
												   fc_value));
		case OIDOID:
		case REGPROCOID:
		case REGPROCEDUREOID:
		case REGOPEROID:
		case REGOPERATOROID:
		case REGCLASSOID:
		case REGTYPEOID:
		case REGCOLLATIONOID:
		case REGCONFIGOID:
		case REGDICTIONARYOID:
		case REGROLEOID:
		case REGNAMESPACEOID:
			/* 我们可以将 OID 视为整数... */
			return (double) DatumGetObjectId(fc_value);
	}

	*fc_failure = true;
	return 0;
}

/*
 * 对于任何字符字符串数据类型执行 convert_to_scalar() 的工作。
 *
 * 字符串数据类型被转换为一个从 0 到 1 的尺度，
 * 我们将字符串的字节视为小数位。
 *
 * 然而，我们不希望基数是 256，因为这往往会产生
 * 夸大的选择性估计；很少有数据库会在每个位置上
 * 发生所有 256 个可能字节值。
 * 相反，使用在边界中看到的最小和最大字节值
 * 作为每个字节的估计范围，经过一些调整来处理
 * 我们可能不会以这种方式看到完整范围的事实。
 *
 * 另一个改进是，在计算缩放值之前，
 * 我们会丢弃三条字符串的任何公共前缀。这使我们能够在遇到
 * 较窄的数据范围时“放大”。一个例子是电话
 * 号数据库，所有值都以相同的区号开头。
 * （实际上，边界将是相邻的直方图边界值，
 * 所以这比你想象的更有可能发生。）
 */
static void fc_convert_string_to_scalar(char *fc_value,
						 double *fc_scaledvalue,
						 char *fc_lobound,
						 double *fc_scaledlobound,
						 char *fc_hibound,
						 double *fc_scaledhibound)
{
	int			fc_rangelo,
				fc_rangehi;
	char	   *fc_sptr;

	fc_rangelo = fc_rangehi = (unsigned char) fc_hibound[0];
	for (fc_sptr = fc_lobound; *fc_sptr; fc_sptr++)
	{
		if (fc_rangelo > (unsigned char) *fc_sptr)
			fc_rangelo = (unsigned char) *fc_sptr;
		if (fc_rangehi < (unsigned char) *fc_sptr)
			fc_rangehi = (unsigned char) *fc_sptr;
	}
	for (fc_sptr = fc_hibound; *fc_sptr; fc_sptr++)
	{
		if (fc_rangelo > (unsigned char) *fc_sptr)
			fc_rangelo = (unsigned char) *fc_sptr;
		if (fc_rangehi < (unsigned char) *fc_sptr)
			fc_rangehi = (unsigned char) *fc_sptr;
	}
	/* 如果范围包括任何大写 ASCII 字符，则使其包括所有 */
	if (fc_rangelo <= 'Z' && fc_rangehi >= 'A')
	{
		if (fc_rangelo > 'A')
			fc_rangelo = 'A';
		if (fc_rangehi < 'Z')
			fc_rangehi = 'Z';
	}
	/* 同理小写字母 */
	if (fc_rangelo <= 'z' && fc_rangehi >= 'a')
	{
		if (fc_rangelo > 'a')
			fc_rangelo = 'a';
		if (fc_rangehi < 'z')
			fc_rangehi = 'z';
	}
	/* 同理数字 */
	if (fc_rangelo <= '9' && fc_rangehi >= '0')
	{
		if (fc_rangelo > '0')
			fc_rangelo = '0';
		if (fc_rangehi < '9')
			fc_rangehi = '9';
	}

	/*
	 * 如果范围包括的字符少于 10 个，假设我们没有足够的
	 * 数据，并使其包含常规 ASCII 集。
	 */
	if (fc_rangehi - fc_rangelo < 9)
	{
		fc_rangelo = ' ';
		fc_rangehi = 127;
	}

	
/*
	 * 现在去除三个字符串的公共前缀。
	 */
	while (*fc_lobound)
	{
		if (*fc_lobound != *fc_hibound || *fc_lobound != *fc_value)
			break;
		fc_lobound++, fc_hibound++, fc_value++;
	}

	/*
	 * 现在我们可以进行转换了。
	 */
	*fc_scaledvalue = fc_convert_one_string_to_scalar(fc_value, fc_rangelo, fc_rangehi);
	*fc_scaledlobound = fc_convert_one_string_to_scalar(fc_lobound, fc_rangelo, fc_rangehi);
	*fc_scaledhibound = fc_convert_one_string_to_scalar(fc_hibound, fc_rangelo, fc_rangehi);
}

static double fc_convert_one_string_to_scalar(char *fc_value, int fc_rangelo, int fc_rangehi)
{
	int			fc_slen = strlen(fc_value);
	double		fc_num,
				fc_denom,
				fc_base;

	if (fc_slen <= 0)
		return 0.0;				/* 空字符串的标量值为 0 */

	/*
	 * 考虑超过十几个字节的字符串似乎没有太大意义。
	 * 由于基数至少为 10，这将使我们获得至少 12 位小数的名义精度，
	 * 这无疑远远超过了这种估计技术所能得到的精度（尤其是在非 C 本地环境中）。
	 * 此外，即使在最大可能的基数 256 下，这也确保了分母不会超过 256^13 = 2.03e31，
	 * 这在任何已知的机器上都不会溢出。
	 */
	if (fc_slen > 12)
		fc_slen = 12;

	/* 将初始字符转换为分数 */
	fc_base = fc_rangehi - fc_rangelo + 1;
	fc_num = 0.0;
	fc_denom = fc_base;
	while (fc_slen-- > 0)
	{
		int			fc_ch = (unsigned char) *fc_value++;

		if (fc_ch < fc_rangelo)
			fc_ch = fc_rangelo - 1;
		else if (fc_ch > fc_rangehi)
			fc_ch = fc_rangehi + 1;
		fc_num += ((double) (fc_ch - fc_rangelo)) / fc_denom;
		fc_denom *= fc_base;
	}

	return fc_num;
}

/*
 * 将一个字符串类型的 Datum 转换为一个 palloc 分配的、以空字符结尾的字符串。
 *
 * 如果失败（例如，不支持的 typid），将 *failure 设置为 true；
 * 否则，该变量不变。（失败时我们将返回 NULL。）
 *
 * 在使用非 C 本地环境时，我们必须在继续之前通过 strxfrm() 处理字符串，
 * 以生成正确的特定于本地环境的结果。
 */
static char * fc_convert_string_datum(Datum fc_value, Oid fc_typid, Oid fc_collid, bool *fc_failure)
{
	char	   *fc_val;

	switch (fc_typid)
	{
		case CHAROID:
			fc_val = (char *) palloc(2);
			fc_val[0] = DatumGetChar(fc_value);
			fc_val[1] = '\0';
			break;
		case BPCHAROID:
		case VARCHAROID:
		case TEXTOID:
			fc_val = TextDatumGetCString(fc_value);
			break;
		case NAMEOID:
			{
				NameData   *fc_nm = (NameData *) DatumGetPointer(fc_value);

				fc_val = pstrdup(NameStr(*fc_nm));
				break;
			}
		default:
			*fc_failure = true;
			return NULL;
	}

	if (!lc_collate_is_c(fc_collid))
	{
		char	   *fc_xfrmstr;
		size_t		fc_xfrmlen;
		size_t		fc_xfrmlen2 PG_USED_FOR_ASSERTS_ONLY;

		/*
		 * XXX：我们可以猜测一个合适的输出缓冲区大小，
		 * 仅在我们的猜测过小时调用 strxfrm 两次。
		 *
		 * XXX：strxfrm 在 Win32 上不支持 UTF-8 编码，可能会返回
		 * 虚假的数据或设置一个错误。这并不是真的问题，除非它崩溃，
		 * 因为它只会给出一个估计误差，而不会造成致命错误。
		 */
		fc_xfrmlen = strxfrm(NULL, fc_val, 0);
#ifdef WIN32

		/*
		 * 在Windows上，strxfrm在发生错误时返回INT_MAX。与其尝试分配如此多的内存（然后失败），不如返回原始字符串，仿佛我们在C区域。
		 */
		if (xfrmlen == INT_MAX)
			return val;
#endif
		fc_xfrmstr = (char *) palloc(fc_xfrmlen + 1);
		fc_xfrmlen2 = strxfrm(fc_xfrmstr, fc_val, fc_xfrmlen + 1);

		/*
		 * 一些系统（例如，glibc）在第二次调用中可能返回比第一次更小的值；因此，Assert必须为<=而不是==。
		 */
		Assert(fc_xfrmlen2 <= fc_xfrmlen);
		pfree(fc_val);
		fc_val = fc_xfrmstr;
	}

	return fc_val;
}

/*
 * 让convert_to_scalar()对任何bytea数据类型有效。
 *
 * 与convert_string_to_scalar非常相似，除了我们不能假设
 * 空终止，因此需要传递显式长度。
 *
 * 此外，关于可能“正常”字符范围的假设已经被移除 - 当前始终使用数据范围0..255。 （也许有一天我们会向pg_statistic添加有关实际字节数据范围的信息。）
 */
static void fc_convert_bytea_to_scalar(Datum fc_value,
						double *fc_scaledvalue,
						Datum fc_lobound,
						double *fc_scaledlobound,
						Datum fc_hibound,
						double *fc_scaledhibound)
{
	bytea	   *fc_valuep = DatumGetByteaPP(fc_value);
	bytea	   *fc_loboundp = DatumGetByteaPP(fc_lobound);
	bytea	   *fc_hiboundp = DatumGetByteaPP(fc_hibound);
	int			fc_rangelo,
				fc_rangehi,
				fc_valuelen = VARSIZE_ANY_EXHDR(fc_valuep),
				fc_loboundlen = VARSIZE_ANY_EXHDR(fc_loboundp),
				fc_hiboundlen = VARSIZE_ANY_EXHDR(fc_hiboundp),
				fc_i,
				fc_minlen;
	unsigned char *fc_valstr = (unsigned char *) VARDATA_ANY(fc_valuep);
	unsigned char *fc_lostr = (unsigned char *) VARDATA_ANY(fc_loboundp);
	unsigned char *fc_histr = (unsigned char *) VARDATA_ANY(fc_hiboundp);

	/*
	 * 假设bytea数据在所有字节值中均匀分布。
	 */
	fc_rangelo = 0;
	fc_rangehi = 255;

	
/*
	 * 现在去除三个字符串的公共前缀。
	 */
	fc_minlen = Min(Min(fc_valuelen, fc_loboundlen), fc_hiboundlen);
	for (fc_i = 0; fc_i < fc_minlen; fc_i++)
	{
		if (*fc_lostr != *fc_histr || *fc_lostr != *fc_valstr)
			break;
		fc_lostr++, fc_histr++, fc_valstr++;
		fc_loboundlen--, fc_hiboundlen--, fc_valuelen--;
	}

	/*
	 * 现在我们可以进行转换了。
	 */
	*fc_scaledvalue = fc_convert_one_bytea_to_scalar(fc_valstr, fc_valuelen, fc_rangelo, fc_rangehi);
	*fc_scaledlobound = fc_convert_one_bytea_to_scalar(fc_lostr, fc_loboundlen, fc_rangelo, fc_rangehi);
	*fc_scaledhibound = fc_convert_one_bytea_to_scalar(fc_histr, fc_hiboundlen, fc_rangelo, fc_rangehi);
}

static double fc_convert_one_bytea_to_scalar(unsigned char *fc_value, int fc_valuelen,
							int fc_rangelo, int fc_rangehi)
{
	double		fc_num,
				fc_denom,
				fc_base;

	if (fc_valuelen <= 0)
		return 0.0;				/* 空字符串的标量值为 0 */

	/*
	 * 由于基数为256，因此无需考虑超过大约10个字符（即使这也显得过多）。
	 */
	if (fc_valuelen > 10)
		fc_valuelen = 10;

	/* 将初始字符转换为分数 */
	fc_base = fc_rangehi - fc_rangelo + 1;
	fc_num = 0.0;
	fc_denom = fc_base;
	while (fc_valuelen-- > 0)
	{
		int			fc_ch = *fc_value++;

		if (fc_ch < fc_rangelo)
			fc_ch = fc_rangelo - 1;
		else if (fc_ch > fc_rangehi)
			fc_ch = fc_rangehi + 1;
		fc_num += ((double) (fc_ch - fc_rangelo)) / fc_denom;
		fc_denom *= fc_base;
	}

	return fc_num;
}

/*
 * 让convert_to_scalar()对任何timevalue数据类型有效。
 *
 * 发生故障时（例如，typid不支持），将*failure设置为true；
 * 否则，该变量不改变。
 */
static double fc_convert_timevalue_to_scalar(Datum fc_value, Oid fc_typid, bool *fc_failure)
{
	switch (fc_typid)
	{
		case TIMESTAMPOID:
			return DatumGetTimestamp(fc_value);
		case TIMESTAMPTZOID:
			return DatumGetTimestampTz(fc_value);
		case DATEOID:
			return date2timestamp_no_overflow(DatumGetDateADT(fc_value));
		case INTERVALOID:
			{
				Interval   *fc_interval = DatumGetIntervalP(fc_value);

				/*
				 * 使用假定的平均月份长度365.25/12.0天将Interval的月份部分转换为天。 不是很准确，但对于我们的目的来说足够好。
				 */
				return fc_interval->time + fc_interval->day * (double) USECS_PER_DAY +
					fc_interval->month * ((DAYS_PER_YEAR / (double) MONTHS_PER_YEAR) * USECS_PER_DAY);
			}
		case TIMEOID:
			return DatumGetTimeADT(fc_value);
		case TIMETZOID:
			{
				TimeTzADT  *fc_timetz = DatumGetTimeTzADTP(fc_value);

				/* 使用GMT等效时间 */
				return (double) (fc_timetz->time + (fc_timetz->zone * 1000000.0));
			}
	}

	*fc_failure = true;
	return 0;
}


/*
 * get_restriction_variable
 *		检查限制子句的参数，看看它是否属于
 *		形式（变量操作伪常量）或（伪常量操作变量），
 *		其中“变量”可以是单个关系中的Var或表达式。 如果是这样，提取有关变量的信息，
 *		并指明它在那一侧以及另一个参数。
 *
 * 输入：
 *	root：规划器信息
 *	args：子句参数列表
 *	varRelid：有关限制选择性函数的规格请参见
 *
 * 输出：（仅当返回true时有效）
 *	*vardata：获取有关变量的信息（请参阅examine_variable）
 *	*other：获取其他子句参数，强行简化为常量
 *	*varonleft：如果变量在左侧，则设置为true，如果在右侧，则设置为false
 *
 * 如果识别到变量，则返回true，否则返回false。
 *
 * 注意：如果子句两侧都有Vars，我们必须失败，因为
 * 调用者期望另一侧将像伪常量一样工作。
 */
bool get_restriction_variable(PlannerInfo *fc_root, List *fc_args, int fc_varRelid,
						 VariableStatData *fc_vardata, Node **fc_other,
						 bool *fc_varonleft)
{
	Node	   *fc_left,
			   *fc_right;
	VariableStatData fc_rdata;

	/* 如果不是二元操作子句则失败（可能不应该发生） */
	if (list_length(fc_args) != 2)
		return false;

	fc_left = (Node *) linitial(fc_args);
	fc_right = (Node *) lsecond(fc_args);

	/*
	 * 检查两侧。 注意，当varRelid非零时，其他关系的Vars将被视为伪常量。
	 */
	examine_variable(fc_root, fc_left, fc_varRelid, fc_vardata);
	examine_variable(fc_root, fc_right, fc_varRelid, &fc_rdata);

	/*
	 * 如果一侧是变量而另一侧不是，我们就赢了。
	 */
	if (fc_vardata->rel && fc_rdata.rel == NULL)
	{
		*fc_varonleft = true;
		*fc_other = estimate_expression_value(fc_root, fc_rdata.var);
		/* 假设这里不需要ReleaseVariableStats(rdata) */
		return true;
	}

	if (fc_vardata->rel == NULL && fc_rdata.rel)
	{
		*fc_varonleft = false;
		*fc_other = estimate_expression_value(fc_root, fc_vardata->var);
		/* 假设这里不需要ReleaseVariableStats(*vardata) */
		*fc_vardata = fc_rdata;
		return true;
	}

	/* 哦，子句结构错误（可能是var操作var） */
	ReleaseVariableStats(*fc_vardata);
	ReleaseVariableStats(fc_rdata);

	return false;
}

/*
 * get_join_variables
 *		对连接子句的每一侧应用examine_variable()。
 *		同时，试图识别连接子句与SpecialJoinInfo的关系是否相同或相反。
 *
 * 如果连接子句是“lhs_var OP rhs_var”，我们将其视为“正常”，
 * 如果是“rhs_var OP lhs_var”，我们将其视为“反向”。 在复杂情况下
 * 当我们无法确定时，我们默认假设它是正常的。
 */
void get_join_variables(PlannerInfo *fc_root, List *fc_args, SpecialJoinInfo *fc_sjinfo,
				   VariableStatData *fc_vardata1, VariableStatData *fc_vardata2,
				   bool *fc_join_is_reversed)
{
	Node	   *fc_left,
			   *fc_right;

	if (list_length(fc_args) != 2)
		elog(ERROR, "join operator should take two arguments");

	fc_left = (Node *) linitial(fc_args);
	fc_right = (Node *) lsecond(fc_args);

	examine_variable(fc_root, fc_left, 0, fc_vardata1);
	examine_variable(fc_root, fc_right, 0, fc_vardata2);

	if (fc_vardata1->rel &&
		bms_is_subset(fc_vardata1->rel->relids, fc_sjinfo->syn_righthand))
		*fc_join_is_reversed = true;	/* var1位于RHS上 */
	else if (fc_vardata2->rel &&
			 bms_is_subset(fc_vardata2->rel->relids, fc_sjinfo->syn_lefthand))
		*fc_join_is_reversed = true;	/* var2位于LHS上 */
	else
		*fc_join_is_reversed = false;
}

/* statext_expressions_load复制元组，因此只需pfree它。 */
static void fc_ReleaseDummy(HeapTuple fc_tuple)
{
	pfree(fc_tuple);
}


/*
 * examine_variable
 * 	 尝试查找有关表达式的统计数据。
 * 	 填充一个 VariableStatData 结构以描述该表达式。
 *
 * 输入：
 * 	root：计划者信息
 * 	node：要检查的表达树
 * 	varRelid：参见限制选择性函数的规范
 *
 * 输出： *vardata的填充如下：
 * 	var：输入表达式（如果是或包含变量，任何二进制重新标记都被剥离；但其他情况下类型保持不变）
 * 	rel：包含变量的 RelOptInfo；如果表达式不包含任何 Vars，则为 NULL（注意，这可能指向子查询的 RelOptInfo，而不是当前查询中的一个）。
 * 	statsTuple：变量的 pg_statistic 条目，如果存在；否则为 NULL。
 * 	freefunc：指向一个释放 statsTuple 的函数的指针。
 * 	vartype：表达式的公开类型；这应该始终与我们正在估计的运算符的声明输入类型匹配。
 * 	atttype, atttypmod：“var”表达式的实际类型/typmod。这通常与变量参数的公开类型相同，但在二进制兼容类型的情况下可能会不同。
 * 	isunique：如果我们能够将 var 匹配到唯一索引或单列 DISTINCT 子句，则为 true，意味着其值在此查询中是唯一的。(注意：这应仅出于统计目的而被信任，因为我们不检查 indimmediate 也不验证适用的相等性定义完全相同。)
 * 	acl_ok：如果当前用户有权限读取支持 pg_statistic 条目的列，则为 true。这由 statistic_proc_security_check() 查询。
 *
 * 调用者负责在退出前调用 ReleaseVariableStats()。
 */
void examine_variable(PlannerInfo *fc_root, Node *fc_node, int fc_varRelid,
				 VariableStatData *fc_vardata)
{
	Node	   *fc_basenode;
	Relids		fc_varnos;
	RelOptInfo *fc_onerel;

	/* 确保我们不在 vardata 中返回悬挂指针 */
	MemSet(fc_vardata, 0, sizeof(VariableStatData));

	/* 保存表达式的公开类型 */
	fc_vardata->vartype = exprType(fc_node);

	/* 查看任何二进制兼容的重新标记 */

	if (IsA(fc_node, RelabelType))
		fc_basenode = (Node *) ((RelabelType *) fc_node)->arg;
	else
		fc_basenode = fc_node;

	/* 简单 Var 的快速路径 */

	if (IsA(fc_basenode, Var) &&
		(fc_varRelid == 0 || fc_varRelid == ((Var *) fc_basenode)->varno))
	{
		Var		   *fc_var = (Var *) fc_basenode;

		/* 设置统计元组以外的结果字段 */
		fc_vardata->var = fc_basenode;	/* 返回没有重新标记的 Var */
		fc_vardata->rel = find_base_rel(fc_root, fc_var->varno);
		fc_vardata->atttype = fc_var->vartype;
		fc_vardata->atttypmod = fc_var->vartypmod;
		fc_vardata->isunique = has_unique_index(fc_vardata->rel, fc_var->varattno);

		/* 尝试查找一些统计数据 */
		fc_examine_simple_variable(fc_root, fc_var, fc_vardata);

		return;
	}

	/*
	 * 好的，这是一个更复杂的表达式。确定变量
	 * 成员资格。请注意，当 varRelid 不为零时，仅考虑该
	 * 关系的 vars 为“真实” vars。
	 */
	fc_varnos = pull_varnos(fc_root, fc_basenode);

	fc_onerel = NULL;

	switch (bms_membership(fc_varnos))
	{
		case BMS_EMPTY_SET:
			/* 根本没有 Vars ... 必须是伪常量子句 */
			break;
		case BMS_SINGLETON:
			if (fc_varRelid == 0 || bms_is_member(fc_varRelid, fc_varnos))
			{
				fc_onerel = find_base_rel(fc_root,
									   (fc_varRelid ? fc_varRelid : bms_singleton_member(fc_varnos)));
				fc_vardata->rel = fc_onerel;
				fc_node = fc_basenode;	/* 剥离任何重新标记 */
			}
			/* 否则将其视为常量 */
			break;
		case BMS_MULTIPLE:
			if (fc_varRelid == 0)
			{
				/* 将其视为连接关系的变量 */
				fc_vardata->rel = find_join_rel(fc_root, fc_varnos);
				fc_node = fc_basenode;	/* 剥离任何重新标记 */
			}
			else if (bms_is_member(fc_varRelid, fc_varnos))
			{
				/* 忽略属于其他关系的变量 */
				fc_vardata->rel = find_base_rel(fc_root, fc_varRelid);
				fc_node = fc_basenode;	/* 剥离任何重新标记 */
				/* 注意：在这里没有表达式索引搜索的意义 */
			}
			/* 否则将其视为常量 */
			break;
	}

	bms_free(fc_varnos);

	fc_vardata->var = fc_node;
	fc_vardata->atttype = exprType(fc_node);
	fc_vardata->atttypmod = exprTypmod(fc_node);

	if (fc_onerel)
	{
		/*
		 * 我们在单一关系的变量中找到了一个表达式。尝试将其匹配
		 * 到表达式索引列，以期找到一些
		 * 统计信息。
		 *
		 * 注意，我们考虑所有索引列，包括 INCLUDE 列，
		 * 因为可能存在这些列的统计信息。但是，对
		 * 唯一性的测试需要更加谨慎。
		 *
		 * XXX 可以想象存在多个与不同
		 * 索引运算族匹配的情况；如果是这样，我们
		 * 需要选择一个与我们正在估算的操作符匹配的。
		 * 稍后修复。
		 */
		ListCell   *fc_ilist;
		ListCell   *fc_slist;

		foreach(fc_ilist, fc_onerel->indexlist)
		{
			IndexOptInfo *fc_index = (IndexOptInfo *) lfirst(fc_ilist);
			ListCell   *fc_indexpr_item;
			int			fc_pos;

			fc_indexpr_item = list_head(fc_index->indexprs);
			if (fc_indexpr_item == NULL)
				continue;		/* 此处没有表达式... */

			for (fc_pos = 0; fc_pos < fc_index->ncolumns; fc_pos++)
			{
				if (fc_index->indexkeys[fc_pos] == 0)
				{
					Node	   *fc_indexkey;

					if (fc_indexpr_item == NULL)
						elog(ERROR, "too few entries in indexprs list");
					fc_indexkey = (Node *) lfirst(fc_indexpr_item);
					if (fc_indexkey && IsA(fc_indexkey, RelabelType))
						fc_indexkey = (Node *) ((RelabelType *) fc_indexkey)->arg;
					if (equal(fc_node, fc_indexkey))
					{
						/*
						 * 找到匹配...它是唯一索引吗？这里的测试
						 * 应该与 has_unique_index() 匹配。
						 */
						if (fc_index->unique &&
							fc_index->nkeycolumns == 1 &&
							fc_pos == 0 &&
							(fc_index->indpred == NIL || fc_index->predOK))
							fc_vardata->isunique = true;

						/*
						 * 它有统计信息吗？我们只考虑
						 * 非部分索引的统计信息，因为部分索引可能
						 * 不反映整个关系的统计信息；上述
						 * 对唯一性的检查是我们从部分索引中获取的唯一信息。
						 *
						 * 然而，索引统计信息钩子必须自行
						 * 决定如何处理部分索引。
						 */
						if (get_index_stats_hook &&
							(*get_index_stats_hook) (fc_root, fc_index->indexoid,
													 fc_pos + 1, fc_vardata))
						{
							/*
							 * 钩子控制了获取统计
							 * 元组。如果它确实提供了一个元组，那么它最好
							 * 有一个 freefunc。
							 */
							if (HeapTupleIsValid(fc_vardata->statsTuple) &&
								!fc_vardata->freefunc)
								elog(ERROR, "no function provided to release variable stats with");
						}
						else if (fc_index->indpred == NIL)
						{
							fc_vardata->statsTuple =
								SearchSysCache3(STATRELATTINH,
												ObjectIdGetDatum(fc_index->indexoid),
												Int16GetDatum(fc_pos + 1),
												BoolGetDatum(false));
							fc_vardata->freefunc = ReleaseSysCache;

							if (HeapTupleIsValid(fc_vardata->statsTuple))
							{
								/* 获取索引的表以进行权限检查 */
								RangeTblEntry *fc_rte;
								Oid			fc_userid;

								fc_rte = planner_rt_fetch(fc_index->rel->relid, fc_root);
								Assert(fc_rte->rtekind == RTE_RELATION);

								/*
								 * 如果设置了 checkAsUser，请使用它，以防我们
								 * 通过视图访问表。
								 */
								fc_userid = fc_rte->checkAsUser ? fc_rte->checkAsUser : GetUserId();

								/*
								 * 为了简单起见，我们坚持要求
								 * 整个表都是可选择的，而不是尝试
								 * 确定索引依赖的列。还要求所有行
								 * 都可以选择——必须没有来自安全屏障视图
								 * 或 RLS 策略的 securityQuals。
								 */
								fc_vardata->acl_ok =
									fc_rte->securityQuals == NIL &&
									(pg_class_aclcheck(fc_rte->relid, fc_userid,
													   ACL_SELECT) == ACLCHECK_OK);

								/*
								 * 如果用户没有权限访问继承的子关系，请检查
								 * 查询中实际提到的表的权限，因为用户
								 * 很可能拥有该权限。注意，父表的整体选择特权
								 * 并不能保证用户可以读取子表的所有列。
								 * 但在实践中，允许访问表达式索引的统计信息
								 * 不太可能导致任何有趣的安全违规行为，因此
								 * 我们还是允许这样做。有关附加注释，请参见
								 * examine_simple_variable() 中的类似代码。
								 */
								if (!fc_vardata->acl_ok &&
									fc_root->append_rel_array != NULL)
								{
									AppendRelInfo *fc_appinfo;
									Index		fc_varno = fc_index->rel->relid;

									fc_appinfo = fc_root->append_rel_array[fc_varno];
									while (fc_appinfo &&
										   planner_rt_fetch(fc_appinfo->parent_relid,
															fc_root)->rtekind == RTE_RELATION)
									{
										fc_varno = fc_appinfo->parent_relid;
										fc_appinfo = fc_root->append_rel_array[fc_varno];
									}
									if (fc_varno != fc_index->rel->relid)
									{
										/* 在此关系上重复权限检查 */
										fc_rte = planner_rt_fetch(fc_varno, fc_root);
										Assert(fc_rte->rtekind == RTE_RELATION);

										fc_userid = fc_rte->checkAsUser ? fc_rte->checkAsUser : GetUserId();

										fc_vardata->acl_ok =
											fc_rte->securityQuals == NIL &&
											(pg_class_aclcheck(fc_rte->relid,
															   fc_userid,
															   ACL_SELECT) == ACLCHECK_OK);
									}
								}
							}
							else
							{
								/* 之后抑制泄露检查 */
								fc_vardata->acl_ok = true;
							}
						}
						if (fc_vardata->statsTuple)
							break;
					}
					fc_indexpr_item = lnext(fc_index->indexprs, fc_indexpr_item);
				}
			}
			if (fc_vardata->statsTuple)
				break;
		}

		/*
		 * 在扩展统计信息中搜索与匹配表达式的一个。
		 * 可能会有多个，因此只需抓取第一个。在未来，
		 * 我们可能会考虑统计目标（并选择最
		 * 准确的统计信息）以及其他参数。
		 */
		foreach(fc_slist, fc_onerel->statlist)
		{
			StatisticExtInfo *fc_info = (StatisticExtInfo *) lfirst(fc_slist);
			RangeTblEntry *fc_rte = planner_rt_fetch(fc_onerel->relid, fc_root);
			ListCell   *fc_expr_item;
			int			fc_pos;

			/*
			 * 一旦找到该表达式的统计信息（无论是
			 * 来自扩展统计信息，还是来自前面循环中的索引），
			 * 就停止。
			 */
			if (fc_vardata->statsTuple)
				break;

			/* 跳过没有每个表达式统计信息的统计信息 */
			if (fc_info->kind != STATS_EXT_EXPRESSIONS)
				continue;

			/* 跳过与 stxdinherit 值不匹配的统计信息 */
			if (fc_info->inherit != fc_rte->inh)
				continue;

			fc_pos = 0;
			foreach(fc_expr_item, fc_info->exprs)
			{
				Node	   *fc_expr = (Node *) lfirst(fc_expr_item);

				Assert(fc_expr);

				/* 在比较之前去除 RelabelType */
				if (fc_expr && IsA(fc_expr, RelabelType))
					fc_expr = (Node *) ((RelabelType *) fc_expr)->arg;

				/* 找到匹配项，看看我们是否可以提取 pg_statistic 行 */
				if (equal(fc_node, fc_expr))
				{
					Oid			fc_userid;

					/*
					 * XXX 不确定我们是否应该在某处缓存元组。
					 * 现在我们每次都只是创建一个新的副本。
					 */
					fc_vardata->statsTuple =
						statext_expressions_load(fc_info->statOid, fc_rte->inh, fc_pos);

					fc_vardata->freefunc = fc_ReleaseDummy;

					/*
					 * 如果设置了 checkAsUser，请使用它，以防我们通过
					 * 视图访问表。
					 */
					fc_userid = fc_rte->checkAsUser ? fc_rte->checkAsUser : GetUserId();

					/*
					 * 为了简单起见，我们坚持要求整个表都是
					 * 可选择的，而不是尝试确定统计对象依赖的列。
					 * 还要求所有行都可以选择——必须没有
					 * 来自安全屏障视图或 RLS 策略的 securityQuals。
					 */
					fc_vardata->acl_ok =
						fc_rte->securityQuals == NIL &&
						(pg_class_aclcheck(fc_rte->relid, fc_userid,
										   ACL_SELECT) == ACLCHECK_OK);

					/*
					 * 如果用户没有权限访问继承的子关系，
					 * 请检查查询中实际提到的表的权限，
					 * 因为用户很可能具有该权限。注意，
					 * 父表的整体选择特权并不能完全保证用户可以读取子表的所有列。
					 * 但在实践中，允许访问表达式统计信息不太可能导致任何
					 * 有趣的安全违规，因此我们还是允许这样做。
					 * 有关附加注释，请参见 examine_simple_variable() 中的类似代码。
					 */
					if (!fc_vardata->acl_ok &&
						fc_root->append_rel_array != NULL)
					{
						AppendRelInfo *fc_appinfo;
						Index		fc_varno = fc_onerel->relid;

						fc_appinfo = fc_root->append_rel_array[fc_varno];
						while (fc_appinfo &&
							   planner_rt_fetch(fc_appinfo->parent_relid,
												fc_root)->rtekind == RTE_RELATION)
						{
							fc_varno = fc_appinfo->parent_relid;
							fc_appinfo = fc_root->append_rel_array[fc_varno];
						}
						if (fc_varno != fc_onerel->relid)
						{
							/* 在此关系上重复权限检查 */
							fc_rte = planner_rt_fetch(fc_varno, fc_root);
							Assert(fc_rte->rtekind == RTE_RELATION);

							fc_userid = fc_rte->checkAsUser ? fc_rte->checkAsUser : GetUserId();

							fc_vardata->acl_ok =
								fc_rte->securityQuals == NIL &&
								(pg_class_aclcheck(fc_rte->relid,
												   fc_userid,
												   ACL_SELECT) == ACLCHECK_OK);
						}
					}

					break;
				}

				fc_pos++;
			}
		}
	}
}

/*
 * examine_simple_variable
 *		处理 examine_variable 的简单 Var
 *
 * 这被分离为一个子例程，以便我们可以递归处理
 * 引用子查询的 Vars。
 *
 * 我们已经填写了 *vardata 的所有字段，除了 stats 元组。
 */
static void fc_examine_simple_variable(PlannerInfo *fc_root, Var *fc_var,
						VariableStatData *fc_vardata)
{
	RangeTblEntry *fc_rte = fc_root->simple_rte_array[fc_var->varno];

	Assert(IsA(fc_rte, RangeTblEntry));

	if (get_relation_stats_hook &&
		(*get_relation_stats_hook) (fc_root, fc_rte, fc_var->varattno, fc_vardata))
	{
		/*
		 * 钩子控制获取 stats 元组。如果它确实提供了
		 * 一个元组，它最好提供一个 freefunc。
		 */
		if (HeapTupleIsValid(fc_vardata->statsTuple) &&
			!fc_vardata->freefunc)
			elog(ERROR, "no function provided to release variable stats with");
	}
	else if (fc_rte->rtekind == RTE_RELATION)
	{
		/*
		 * 普通表或继承附加关系的父表，因此在 pg_statistic 中查找
		 * 列
		 */
		fc_vardata->statsTuple = SearchSysCache3(STATRELATTINH,
											  ObjectIdGetDatum(fc_rte->relid),
											  Int16GetDatum(fc_var->varattno),
											  BoolGetDatum(fc_rte->inh));
		fc_vardata->freefunc = ReleaseSysCache;

		if (HeapTupleIsValid(fc_vardata->statsTuple))
		{
			Oid			fc_userid;

			/*
			 * 检查用户是否有权限读取此列。我们要求
			 * 所有行都可以访问，因此不能有来自安全障碍视图或
			 * RLS 策略的 securityQuals。如果设置了 checkAsUser，
			 * 则使用它，以防我们通过视图访问表。
			 */
			fc_userid = fc_rte->checkAsUser ? fc_rte->checkAsUser : GetUserId();

			fc_vardata->acl_ok =
				fc_rte->securityQuals == NIL &&
				((pg_class_aclcheck(fc_rte->relid, fc_userid,
									ACL_SELECT) == ACLCHECK_OK) ||
				 (pg_attribute_aclcheck(fc_rte->relid, fc_var->varattno, fc_userid,
										ACL_SELECT) == ACLCHECK_OK));

			/*
			 * 如果用户没有权限访问继承子关系或特定的此属性，
			 * 请检查查询中实际提到的表/列的权限，因为很可能用户有
			 * 该权限（否则查询将在运行时失败），如果用户可以读取
			 * 那里的列，则他也可以获取子表的值。为此，我们必须查明
			 * 子关系的属性对应于哪个根父属性。
			 */
			if (!fc_vardata->acl_ok && fc_var->varattno > 0 &&
				fc_root->append_rel_array != NULL)
			{
				AppendRelInfo *fc_appinfo;
				Index		fc_varno = fc_var->varno;
				int			fc_varattno = fc_var->varattno;
				bool		fc_found = false;

				fc_appinfo = fc_root->append_rel_array[fc_varno];

				/*
				 * 分区映射到其直接父级，而不是根父级，因此必须准备好向上遍历多个
				 * AppendRelInfos。但如果遇到不是
				 * RTE_RELATION 的父级，则停止 —— 那是一个扁平化的 UNION ALL 子查询，而不是
				 * 继承父级。
				 */
				while (fc_appinfo &&
					   planner_rt_fetch(fc_appinfo->parent_relid,
										fc_root)->rtekind == RTE_RELATION)
				{
					int			fc_parent_varattno;

					fc_found = false;
					if (fc_varattno <= 0 || fc_varattno > fc_appinfo->num_child_cols)
						break;	/* 安全检查 */
					fc_parent_varattno = fc_appinfo->parent_colnos[fc_varattno - 1];
					if (fc_parent_varattno == 0)
						break;	/* Var 是子级的局部 */

					fc_varno = fc_appinfo->parent_relid;
					fc_varattno = fc_parent_varattno;
					fc_found = true;

					/* 如果父项本身也是子项，继续向上。 */
					fc_appinfo = fc_root->append_rel_array[fc_varno];
				}

				/*
				 * 在少数情况下，Var 可能是子表的本地，用这种情况下，我们必须接受无法访问此
				 * 列的统计信息。
				 */
				if (!fc_found)
					return;

				/* 在这个父关系和列上重复访问检查 */
				fc_rte = planner_rt_fetch(fc_varno, fc_root);
				Assert(fc_rte->rtekind == RTE_RELATION);

				fc_userid = fc_rte->checkAsUser ? fc_rte->checkAsUser : GetUserId();

				fc_vardata->acl_ok =
					fc_rte->securityQuals == NIL &&
					((pg_class_aclcheck(fc_rte->relid, fc_userid,
										ACL_SELECT) == ACLCHECK_OK) ||
					 (pg_attribute_aclcheck(fc_rte->relid, fc_varattno, fc_userid,
											ACL_SELECT) == ACLCHECK_OK));
			}
		}
		else
		{
			/* 抑制后续可能的泄漏检查 */
			fc_vardata->acl_ok = true;
		}
	}
	else if (fc_rte->rtekind == RTE_SUBQUERY && !fc_rte->inh)
	{
		/*
		 * 普通子查询（不是已转换为 appendrel 的子查询）。
		 */
		Query	   *fc_subquery = fc_rte->subquery;
		RelOptInfo *fc_rel;
		TargetEntry *fc_ste;

		/*
		 * 如果是整个行变量而不是普通列引用，则放弃处理。
		 */
		if (fc_var->varattno == InvalidAttrNumber)
			return;

		/*
		 * 如果子查询使用集合操作或 GROUP BY，则放弃处理，因为这些操作会将基础列的
		 * 统计信息混淆得无法识别。（集合操作尤其棘手；如果我们继续下去，将只返回与
		 * 最左边子查询相关的统计信息。）DISTINCT 也是一个问题，但我们稍后检查，因为
		 * 仍有可能在这方面获得一些信息。
		 */
		if (fc_subquery->setOperations ||
			fc_subquery->groupClause ||
			fc_subquery->groupingSets)
			return;

		/*
		 * 好的，获取子查询的 RelOptInfo。注意，我们不会更改 vardata 中返回的
		 * rel，因为调用者期望它是调用者查询级别的 rel。由于我们可能已经在递归，
		 * 因此也不能使用该 rel 指针，而必须重新查找 Var 的 rel。
		 */
		fc_rel = find_base_rel(fc_root, fc_var->varno);

		/* 如果子查询尚未规划，必须放弃处理 */
		if (fc_rel->subroot == NULL)
			return;
		Assert(IsA(fc_rel->subroot, PlannerInfo));

		/*
		 * 转向由规划器篡改的子查询。上述测试可以查看预规划版本，但现在我们需要
		 * 一个将引用子根的活动 RelOptInfos 的 Var。例如，如果在规划期间发生了子查询
		 * 向上提升，目标列表中的 Vars 可能已被替换，我们需要查看替换表达式。
		 */
		fc_subquery = fc_rel->subroot->parse;
		Assert(IsA(fc_subquery, Query));

		/* 获取上层 Var 引用的子查询输出表达式 */
		fc_ste = get_tle_by_resno(fc_subquery->targetList, fc_var->varattno);
		if (fc_ste == NULL || fc_ste->resjunk)
			elog(ERROR, "subquery %s does not have attribute %d",
				 fc_rte->eref->aliasname, fc_var->varattno);
		fc_var = (Var *) fc_ste->expr;

		/*
		 * 如果子查询使用 DISTINCT，我们无法利用该变量的任何统计信息……但是，如果
		 * 它是唯一的 DISTINCT 列，我们有权认为它是唯一的。我们以这种方式进行测试，
		 * 以便适用于涉及 DISTINCT ON 的情况。
		 */
		if (fc_subquery->distinctClause)
		{
			if (list_length(fc_subquery->distinctClause) == 1 &&
				targetIsInSortList(fc_ste, InvalidOid, fc_subquery->distinctClause))
				fc_vardata->isunique = true;
			/* 无法进一步处理 */
			return;
		}

		/*
		 * 如果子查询来自具有 security_barrier 属性的视图，我们必须不查看变量的
		 * 统计信息，尽管注意到 EXISTS DISTINCT 子句似乎没问题。因此在这里停止。
		 *
		 * 这可能比必要的限制要严格；对于选择性估算器（这是一个 C 函数，因此无所不能）
		 * 来说，查看统计信息是可以的。但许多选择性估算器会乐意 *调用操作符函数* 来尝试
		 * 计算出一个好的估计 —— 这是不可以的。因此目前为止，不要深入挖掘统计。
		 */
		if (fc_rte->security_barrier)
			return;

		/* 只能处理子查询查询级别的简单 Var */
		if (fc_var && IsA(fc_var, Var) &&
			fc_var->varlevelsup == 0)
		{
			/*
			 * 好的，递归进入子查询。注意，原始的 vardata->isunique（这肯定为假）在这种
			 * 情况下保持不变。这就是我们想要的，因为即使基础列是唯一的，子查询可能已
			 * 与其他表联接以创建重复数据。
			 */
			fc_examine_simple_variable(fc_rel->subroot, fc_var, fc_vardata);
		}
	}
	else
	{
		/*
		 * 否则，Var 来自一个 FUNCTION、VALUES 或 CTE RTE。（我们在这里看不到 RTE_JOIN，
		 * 因为连接别名 Vars 已经被扁平化。）我们对函数输出的处理不多，但或许将来
		 * 可以对 VALUES 和/或 CTE 更聪明一些。
		 */
	}
}

/*
 * 检查是否允许调用 func_oid 并传递 vardata 中的一些
 * pg_statistic 数据。我们允许这样做，如果用户对基于 pg_statistic 数据的表或列有
 * SELECT 权限，或者如果该函数被标记为“防泄漏”。
 */
bool statistic_proc_security_check(VariableStatData *fc_vardata, Oid fc_func_oid)
{
	if (fc_vardata->acl_ok)
		return true;

	if (!OidIsValid(fc_func_oid))
		return false;

	if (get_func_leakproof(fc_func_oid))
		return true;

	ereport(DEBUG2,
			(errmsg_internal("not using statistics because function \"%s\" is not leak-proof",
							 get_func_name(fc_func_oid))));
	return false;
}

/*
 * get_variable_numdistinct
 *	  估计变量的不同值的数量。
 *
 * vardata: examine_variable 的结果
 * *isdefault: 如果结果是默认值而不是基于任何有意义的内容，则设置为 true。
 *
 * 注意：小心产生一个正整数结果，因为调用者可能会将结果与精确的整数计数进行
 * 比较，或者可能会对其进行除法操作。
 */
double get_variable_numdistinct(VariableStatData *fc_vardata, bool *fc_isdefault)
{
	double		fc_stadistinct;
	double		fc_stanullfrac = 0.0;
	double		fc_ntuples;

	*fc_isdefault = false;

	/*
	 * 确定要使用的 stadistinct 值。在某些情况下，即使没有 pg_statistic 条目，我们也可以
	 * 获得估计，或者可以获得比 pg_statistic 中更好的值。如果能找到 stanullfrac 也
	 * 提取它（否则，假设没有 null，因为没有更好的想法）。
	 */
	if (HeapTupleIsValid(fc_vardata->statsTuple))
	{
		/* 使用 pg_statistic 条目 */
		Form_pg_statistic fc_stats;

		fc_stats = (Form_pg_statistic) GETSTRUCT(fc_vardata->statsTuple);
		fc_stadistinct = fc_stats->stadistinct;
		fc_stanullfrac = fc_stats->stanullfrac;
	}
	else if (fc_vardata->vartype == BOOLOID)
	{
		/*
		 * 特殊处理布尔列：推测为两个不同的值。
		 *
		 * 是否还有其他数据类型需要我们为其提供特殊的估计？
		 */
		fc_stadistinct = 2.0;
	}
	else if (fc_vardata->rel && fc_vardata->rel->rtekind == RTE_VALUES)
	{
		/*
		 * 如果 Var 代表 VALUES RTE 的一列，假设它是唯一的。
		 * 当然这可能是非常错误的，但在良好编写的查询中应该趋向于
		 * 为真。我们可以考虑检查 VALUES 的内容以获取一些真实的
		 * 统计信息；但这仅在条目全是常量的情况下有效，而且成本
		 * 相当高。
		 */
		fc_stadistinct = -1.0;		/* 唯一（且全部非空） */
	}
	else
	{
		/* 
		 * 我们不为系统列保持统计信息，但在某些情况下我们仍然可以推断出唯一性。
		 */
		if (fc_vardata->var && IsA(fc_vardata->var, Var))
		{
			switch (((Var *) fc_vardata->var)->varattno)
			{
				case SelfItemPointerAttributeNumber:
					fc_stadistinct = -1.0; /* 唯一（且全部非空） */
					break;
				case TableOidAttributeNumber:
					fc_stadistinct = 1.0;	/* 仅有1个值 */
					break;
				default:
					fc_stadistinct = 0.0;	/* 意思是“未知” */
					break;
			}
		}
		else
			fc_stadistinct = 0.0;	/* 意思是“未知” */

		/* 
		 * XXX 考虑在表达式上使用 estimate_num_groups 吗？
		 */
	}

	/* 
	 * 如果变量有唯一索引或 DISTINCT 子句，假设它是唯一的，无论 pg_statistic 说什么；统计信息可能已经过时，或者我们可能已经找到了部分唯一索引，证明该变量在这个查询中是唯一的。然而，我们最好仍然相信 null- fraction 统计信息。
	 */
	if (fc_vardata->isunique)
		fc_stadistinct = -1.0 * (1.0 - fc_stanullfrac);

	/* 
	 * 如果我们有一个绝对的估计，就使用它。
	 */
	if (fc_stadistinct > 0.0)
		return clamp_row_est(fc_stadistinct);

	/*
	 * 否则我们需要获取关系大小；如果不可用则放弃。
	 */
	if (fc_vardata->rel == NULL)
	{
		*fc_isdefault = true;
		return DEFAULT_NUM_DISTINCT;
	}
	fc_ntuples = fc_vardata->rel->tuples;
	if (fc_ntuples <= 0.0)
	{
		*fc_isdefault = true;
		return DEFAULT_NUM_DISTINCT;
	}

	/*
	 * 如果我们有相对估计值，使用它。
	 */
	if (fc_stadistinct < 0.0)
		return clamp_row_est(-fc_stadistinct * fc_ntuples);

	/*
	 * 如果没有数据，如果表很小，则估计 ndistinct = ntuples，否则使用默认值。
	 * 我们使用 DEFAULT_NUM_DISTINCT 作为 "小" 的截止值，这样行为就不会出现不连续。
	 */
	if (fc_ntuples < DEFAULT_NUM_DISTINCT)
		return clamp_row_est(fc_ntuples);

	*fc_isdefault = true;
	return DEFAULT_NUM_DISTINCT;
}

/*
 * get_variable_range
 *		估计指定变量的最小值和最大值。
 *		如果成功，将值存储在 *min 和 *max 中，并返回 true。
 *		如果没有可用数据，返回 false。
 *
 * sortop 是要使用的 "<" 比较运算符。一般来说，这应该是 "<" 而不是 ">"，
 * 因为只有前者可能在 pg_statistic 中找到。
 * 还必须指定排序规则。
 */
static bool get_variable_range(PlannerInfo *fc_root, VariableStatData *fc_vardata,
				   Oid fc_sortop, Oid fc_collation,
				   Datum *fc_min, Datum *fc_max)
{
	Datum		fc_tmin = 0;
	Datum		fc_tmax = 0;
	bool		fc_have_data = false;
	int16		fc_typLen;
	bool		fc_typByVal;
	Oid			fc_opfuncoid;
	FmgrInfo	fc_opproc;
	AttStatsSlot fc_sslot;

	/*
	 * XXX 很诱人尝试使用实际的列最小值和最大值，
	 * 如果我们可以通过索引探测相对便宜地获得它们。
	 * 但是，由于这个函数在连接规划期间被多次调用，这可能会对规划速度产生
	 * 不愉快的影响。在启用之前需要更多调查。
	 */
#ifdef NOT_USED
	if (get_actual_variable_range(fc_root, fc_vardata, fc_sortop, fc_collation, fc_min, fc_max))
		return true;
#endif

	if (!HeapTupleIsValid(fc_vardata->statsTuple))
	{
		/* 没有可用统计信息，因此默认结果 */
		return false;
	}

	/*
	 * 如果我们无法将 sortop 应用到统计数据中，就失败。
	 * 原则上，如果有直方图而没有 MCVs，我们可以在不应用 sortop 的情况下
	 * 返回直方图的端点……但这可能不值得尝试，因为调用者想要对
	 * 端点做的任何事情也可能会失败安全检查。
	 */
	if (!statistic_proc_security_check(fc_vardata,
									   (fc_opfuncoid = get_opcode(fc_sortop))))
		return false;

	fc_opproc.fn_oid = InvalidOid; /* 将此标记为尚未查找 */

	get_typlenbyval(fc_vardata->atttype, &fc_typLen, &fc_typByVal);

	/*
	 * 如果有按我们想要的顺序排列的直方图，获取第一个和最后一个值。
	 */
	if (get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
						 STATISTIC_KIND_HISTOGRAM, fc_sortop,
						 ATTSTATSSLOT_VALUES))
	{
		if (fc_sslot.stacoll == fc_collation && fc_sslot.nvalues > 0)
		{
			fc_tmin = datumCopy(fc_sslot.values[0], fc_typByVal, fc_typLen);
			fc_tmax = datumCopy(fc_sslot.values[fc_sslot.nvalues - 1], fc_typByVal, fc_typLen);
			fc_have_data = true;
		}
		free_attstatsslot(&fc_sslot);
	}

	/*
	 * 否则，如果有其他顺序的直方图，扫描它并根据我们想要的顺序
	 * 获取最小值和最大值。当然，这可能会找不到根据我们
	 * 的顺序真正极端的值，但这比忽略可用数据要好。
	 */
	if (!fc_have_data &&
		get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
						 STATISTIC_KIND_HISTOGRAM, InvalidOid,
						 ATTSTATSSLOT_VALUES))
	{
		get_stats_slot_range(&fc_sslot, fc_opfuncoid, &fc_opproc,
							 fc_collation, fc_typLen, fc_typByVal,
							 &fc_tmin, &fc_tmax, &fc_have_data);
		free_attstatsslot(&fc_sslot);
	}

	/*
	 * 如果我们有最常见值的信息，查找极端 MCVs。
	 * 即使我们也有直方图，这也需要，因为直方图排除了 MCVs。
	 * 然而，如果我们 *仅* 有 MCVs 而没有直方图，我们应该对
	 * 决定这是否是数据的完整表示持谨慎态度。
	 * 仅在 MCVs 代表整个表（在四舍五入误差范围内）时才继续。
	 */
	if (get_attstatsslot(&fc_sslot, fc_vardata->statsTuple,
						 STATISTIC_KIND_MCV, InvalidOid,
						 fc_have_data ? ATTSTATSSLOT_VALUES :
						 (ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS)))
	{
		bool		fc_use_mcvs = fc_have_data;

		if (!fc_have_data)
		{
			double		fc_sumcommon = 0.0;
			double		fc_nullfrac;
			int			fc_i;

			for (fc_i = 0; fc_i < fc_sslot.nnumbers; fc_i++)
				fc_sumcommon += fc_sslot.numbers[fc_i];
			fc_nullfrac = ((Form_pg_statistic) GETSTRUCT(fc_vardata->statsTuple))->stanullfrac;
			if (fc_sumcommon + fc_nullfrac > 0.99999)
				fc_use_mcvs = true;
		}

		if (fc_use_mcvs)
			get_stats_slot_range(&fc_sslot, fc_opfuncoid, &fc_opproc,
								 fc_collation, fc_typLen, fc_typByVal,
								 &fc_tmin, &fc_tmax, &fc_have_data);
		free_attstatsslot(&fc_sslot);
	}

	*fc_min = fc_tmin;
	*fc_max = fc_tmax;
	return fc_have_data;
}

/*
 * get_stats_slot_range: 扫描 sslot 以获取最小/最大值
 *
 * get_variable_range 的子例程：根据我们在统计数组中找到的内容更新 min/max/have_data。
 */
static void get_stats_slot_range(AttStatsSlot *fc_sslot, Oid fc_opfuncoid, FmgrInfo *fc_opproc,
					 Oid fc_collation, int16 fc_typLen, bool fc_typByVal,
					 Datum *fc_min, Datum *fc_max, bool *fc_p_have_data)
{
	Datum		fc_tmin = *fc_min;
	Datum		fc_tmax = *fc_max;
	bool		fc_have_data = *fc_p_have_data;
	bool		fc_found_tmin = false;
	bool		fc_found_tmax = false;

	/* 查找比较函数，如果我们还没有这样做的话 */
	if (fc_opproc->fn_oid != fc_opfuncoid)
		fmgr_info(fc_opfuncoid, fc_opproc);

	/* 扫描所有插槽的值 */
	for (int fc_i = 0; fc_i < fc_sslot->nvalues; fc_i++)
	{
		if (!fc_have_data)
		{
			fc_tmin = fc_tmax = fc_sslot->values[fc_i];
			fc_found_tmin = fc_found_tmax = true;
			*fc_p_have_data = fc_have_data = true;
			continue;
		}
		if (DatumGetBool(FunctionCall2Coll(fc_opproc,
										   fc_collation,
										   fc_sslot->values[fc_i], fc_tmin)))
		{
			fc_tmin = fc_sslot->values[fc_i];
			fc_found_tmin = true;
		}
		if (DatumGetBool(FunctionCall2Coll(fc_opproc,
										   fc_collation,
										   fc_tmax, fc_sslot->values[fc_i])))
		{
			fc_tmax = fc_sslot->values[fc_i];
			fc_found_tmax = true;
		}
	}

	/*
	 * 如果我们找到了新的极端值，复制插槽的值。
	 */
	if (fc_found_tmin)
		*fc_min = datumCopy(fc_tmin, fc_typByVal, fc_typLen);
	if (fc_found_tmax)
		*fc_max = datumCopy(fc_tmax, fc_typByVal, fc_typLen);
}


/*
 * get_actual_variable_range
 *		尝试通过查找合适的 btree 索引来识别指定变量的当前 *实际* 最小值和/或最大值，
 *		并获取其低值和/或高值。
 *		如果成功，将值存储在 *min 和 *max 中，并返回 true。
 *		（如果不需要该端点，则指针可以为 NULL。）
 *		如果不成功，返回 false。
 *
 * sortop 是要使用的 "<" 比较运算符。
 * collation 是所需的排序规则。
 */
static bool get_actual_variable_range(PlannerInfo *fc_root, VariableStatData *fc_vardata,
						  Oid fc_sortop, Oid fc_collation,
						  Datum *fc_min, Datum *fc_max)
{
	bool		fc_have_data = false;
	RelOptInfo *fc_rel = fc_vardata->rel;
	RangeTblEntry *fc_rte;
	ListCell   *fc_lc;

	/* 如果没有关系或没有索引就没有希望 */
	if (fc_rel == NULL || fc_rel->indexlist == NIL)
		return false;
	/* 如果有索引，它必须是一个普通关系 */
	fc_rte = fc_root->simple_rte_array[fc_rel->relid];
	Assert(fc_rte->rtekind == RTE_RELATION);

	/* 搜索索引以查看是否有任何与我们的问题匹配 */
	foreach(fc_lc, fc_rel->indexlist)
	{
		IndexOptInfo *fc_index = (IndexOptInfo *) lfirst(fc_lc);
		ScanDirection fc_indexscandir;

		/* 忽略非-btree 索引 */
		if (fc_index->relam != BTREE_AM_OID)
			continue;

		/*
		 * 忽略部分索引---我们只想要覆盖整个
		 * 关系的统计数据。
		 */
		if (fc_index->indpred != NIL)
			continue;

		/*
		 * 索引列表可能包括通过 get_relation_info hook 插入的假设索引---不要尝试访问它们。
		 */
		if (fc_index->hypothetical)
			continue;

		/*
		 * 第一个索引列必须与所需变量、sortop 和
		 * collation 匹配---但是我们可以使用降序索引。
		 */
		if (fc_collation != fc_index->indexcollations[0])
			continue;			/* 首先测试，因为它是最便宜的 */
		if (!match_index_to_operand(fc_vardata->var, 0, fc_index))
			continue;
		switch (get_op_opfamily_strategy(fc_sortop, fc_index->sortopfamily[0]))
		{
			case BTLessStrategyNumber:
				if (fc_index->reverse_sort[0])
					fc_indexscandir = BackwardScanDirection;
				else
					fc_indexscandir = ForwardScanDirection;
				break;
			case BTGreaterStrategyNumber:
				if (fc_index->reverse_sort[0])
					fc_indexscandir = ForwardScanDirection;
				else
					fc_indexscandir = BackwardScanDirection;
				break;
			default:
				/* 索引不匹配 sortop */
				continue;
		}

		/*
		 * 找到一个合适的索引来提取数据。
		 * 设置一些可以被 get_actual_variable_endpoint 的两个调用使用的数据。
		 */
		{
			MemoryContext fc_tmpcontext;
			MemoryContext fc_oldcontext;
			Relation	fc_heapRel;
			Relation	fc_indexRel;
			TupleTableSlot *fc_slot;
			int16		fc_typLen;
			bool		fc_typByVal;
			ScanKeyData fc_scankeys[1];

			/* 确保任何杂物在完成后被回收 */
			fc_tmpcontext = AllocSetContextCreate(CurrentMemoryContext,
											   "get_actual_variable_range workspace",
											   ALLOCSET_DEFAULT_SIZES);
			fc_oldcontext = MemoryContextSwitchTo(fc_tmpcontext);

			/*
			 * 打开表和索引，以便我们可以从中读取。
			 * 我们应该已经对每一个类型的锁。
			 */
			fc_heapRel = table_open(fc_rte->relid, NoLock);
			fc_indexRel = index_open(fc_index->indexoid, NoLock);

			/* 构建一些执行索引扫描所需的内容 */
			fc_slot = table_slot_create(fc_heapRel, NULL);
			get_typlenbyval(fc_vardata->atttype, &fc_typLen, &fc_typByVal);

			/* 设置一个 IS NOT NULL 扫描键，以便我们忽略空值 */
			ScanKeyEntryInitialize(&fc_scankeys[0],
								   SK_ISNULL | SK_SEARCHNOTNULL,
								   1,	/* 要扫描的索引列 */
								   InvalidStrategy, /* 没有策略 */
								   InvalidOid,	/* 没有策略子类型 */
								   InvalidOid,	/* 没有排序 */
								   InvalidOid,	/* 没有注册过程 */
								   (Datum) 0);	/* constant */

			/* 如果请求最小值... */
			if (fc_min)
			{
				fc_have_data = get_actual_variable_endpoint(fc_heapRel,
														 fc_indexRel,
														 fc_indexscandir,
														 fc_scankeys,
														 fc_typLen,
														 fc_typByVal,
														 fc_slot,
														 fc_oldcontext,
														 fc_min);
			}
			else
			{
				/* 如果没有请求最小值，仍然想获取最大值 */
				fc_have_data = true;
			}

			/* 如果请求最大值，并且我们还没有失败... */
			if (fc_max && fc_have_data)
			{
				/* 以相反的方向扫描；其他一切相同 */
				fc_have_data = get_actual_variable_endpoint(fc_heapRel,
														 fc_indexRel,
														 -fc_indexscandir,
														 fc_scankeys,
														 fc_typLen,
														 fc_typByVal,
														 fc_slot,
														 fc_oldcontext,
														 fc_max);
			}

			/* 清理所有内容 */
			ExecDropSingleTupleTableSlot(fc_slot);

			index_close(fc_indexRel, NoLock);
			table_close(fc_heapRel, NoLock);

			MemoryContextSwitchTo(fc_oldcontext);
			MemoryContextDelete(fc_tmpcontext);

			/* 我们完成了 */
			break;
		}
	}

	return fc_have_data;
}

/*
 * 从指定索引获取一个端点数据（根据indexscandir为最小值或最大值）。如果成功，返回真；如果不成功，返回假。
 * 成功时，端点值存储到*endpointDatum（并复制到outercontext）。
 *
 * scankeys是一个具有1个元素的scankey数组，用于拒绝空值。
 * typLen/typByVal描述索引第一列的数据类型。
 * tableslot是一个适合保存表元组的槽，以防我们需要探测堆。
 * （我们可以在本地计算这些值，但那样会在get_actual_variable_range需要最小值和最大值时计算两次。）
 *
 * 失败发生在索引为空，或者我们决定找到合适的元组花费太长时间时。
 */
static bool get_actual_variable_endpoint(Relation fc_heapRel,
							 Relation fc_indexRel,
							 ScanDirection fc_indexscandir,
							 ScanKey fc_scankeys,
							 int16 fc_typLen,
							 bool fc_typByVal,
							 TupleTableSlot *fc_tableslot,
							 MemoryContext fc_outercontext,
							 Datum *fc_endpointDatum)
{
	bool		fc_have_data = false;
	SnapshotData fc_SnapshotNonVacuumable;
	IndexScanDesc fc_index_scan;
	Buffer		fc_vmbuffer = InvalidBuffer;
	BlockNumber fc_last_heap_block = InvalidBlockNumber;
	int			fc_n_visited_heap_pages = 0;
	ItemPointer fc_tid;
	Datum		fc_values[INDEX_MAX_KEYS];
	bool		fc_isnull[INDEX_MAX_KEYS];
	MemoryContext fc_oldcontext;

	/*
	 * 我们为此使用索引仅扫描机制。对于大多数静态表来说，这是一个优势，因为它避免了对堆的访问。对于动态数据来说也是一个优势，但原因不那么明显；请继续阅读以获取更多细节。
	 *
	 * 原则上，我们应该使用我们当前活动的快照扫描索引，这是我们对查询在执行时将看到的内容的最佳近似。但是，如果在运行查询之前拍摄了新的快照，则这不会是确切的；如果索引的开头或结尾存在许多最近死掉或未提交的行，这可能会非常昂贵（因为我们将费力地获取每一条并拒绝它）。相反，我们使用SnapshotNonVacuumable。它将接受最近死掉和未提交的行以及普通可见行。另一方面，它将拒绝已知死掉的行，因此当极值被删除时，不会给出虚假的答案（除非删除非常近期）；这种情况促使我们在此不使用SnapshotAny。
	 *
	 * 此外一个关键点是，SnapshotNonVacuumable，在GlobalVisTestFor(heapRel)作为视野的情况下，产生的条件与indexscan将使用以决定索引条目是否可被删除的条件相反（见heap_hot_search_buffer()）。因此，如果快照拒绝了一个元组（或更准确地说，拒绝了HOT链的所有元组），并且我们必须继续扫描它，我们知道indexscan将标记该索引条目为已删除。这意味着下一次get_actual_variable_endpoint()调用将不必重新考虑该索引条目。这种方式在此函数在规划过程中被大量使用时，避免了重复工作。
	 *
	 * 但是，使用SnapshotNonVacuumable会带来自身的风险。在一个最近创建的索引中，一些索引条目可能指向“破损”的HOT链，其中并不是所有的元组版本都包含与索引条目匹配的数据。活跃元组版本确实与索引匹配，但SnapshotNonVacuumable可以接受最近死掉且不匹配的元组版本。因此，如果我们从所选的堆元组中获取数据，我们可能会得到一个虚假的答案，与索引极值不接近，甚至可能是NULL。我们避免这种风险，因为我们从索引条目中取得数据而不是堆。
	 *
	 * 尽管如此，当我们在索引末尾发现许多不可见的元组时，仍然会出现一些情况。我们不想在这里花费大量时间，因此一旦我们读取了太多的堆页面，就放弃。当我们因此失败时，调用者将最终使用pg_statistic中记录的任何极值。
	 */
	InitNonVacuumableSnapshot(fc_SnapshotNonVacuumable,
							  GlobalVisTestFor(fc_heapRel));

	fc_index_scan = index_beginscan(fc_heapRel, fc_indexRel,
								 &fc_SnapshotNonVacuumable,
								 1, 0);
	/* 为索引仅扫描进行设置 */
	fc_index_scan->xs_want_itup = true;
	index_rescan(fc_index_scan, fc_scankeys, 1, NULL, 0);

	/* 在指定方向中获取第一个/下一个元组 */
	while ((fc_tid = index_getnext_tid(fc_index_scan, fc_indexscandir)) != NULL)
	{
		BlockNumber fc_block = ItemPointerGetBlockNumber(fc_tid);

		if (!VM_ALL_VISIBLE(fc_heapRel,
							fc_block,
							&fc_vmbuffer))
		{
			/* 糟糕，我们必须访问堆以检查可见性 */
			if (!index_fetch_heap(fc_index_scan, fc_tableslot))
			{
				/*
				 * 对于这个索引条目没有可见的元组，因此我们需要
				 * 前进到下一个条目。在这样做之前，计算堆
				 * 页面获取次数，如果我们做得太多，就放弃。
				 *
				 * 如果这是与前一个元组相同的堆页面，则不收取页面获取费用。这是保守的做法，因为其他最近访问的页面可能仍在缓冲区中；但对于这个启发式来说已经足够好。
				 */
#define VISITED_PAGES_LIMIT 100

				if (fc_block != fc_last_heap_block)
				{
					fc_last_heap_block = fc_block;
					fc_n_visited_heap_pages++;
					if (fc_n_visited_heap_pages > VISITED_PAGES_LIMIT)
						break;
				}

				continue;		/* 没有可见元组，尝试下一个索引条目 */
			}

			/* 我们实际上不需要堆元组做任何事情 */
			ExecClearTuple(fc_tableslot);

			/*
			 * 我们并不关心HOT链中是否有多个可见元组；如果有任何一个可见，那就足够了。
			 */
		}

		/*
		 * 我们期望btree以IndexTuple而不是HeapTuple格式返回数据。这也不是有损的。
		 */
		if (!fc_index_scan->xs_itup)
			elog(ERROR, "no data returned for index-only scan");
		if (fc_index_scan->xs_recheck)
			elog(ERROR, "unexpected recheck indication from btree");

		/* 可以析构索引元组 */
		index_deform_tuple(fc_index_scan->xs_itup,
						   fc_index_scan->xs_itupdesc,
						   fc_values, fc_isnull);

		/* 不应该得到NULL，但要小心 */
		if (fc_isnull[0])
			elog(ERROR, "found unexpected null value in index \"%s\"",
				 RelationGetRelationName(fc_indexRel));

		/* 将索引列值复制到调用者的上下文中 */
		fc_oldcontext = MemoryContextSwitchTo(fc_outercontext);
		*fc_endpointDatum = datumCopy(fc_values[0], fc_typByVal, fc_typLen);
		MemoryContextSwitchTo(fc_oldcontext);
		fc_have_data = true;
		break;
	}

	if (fc_vmbuffer != InvalidBuffer)
		ReleaseBuffer(fc_vmbuffer);
	index_endscan(fc_index_scan);

	return fc_have_data;
}


/*
 * 查找连接的输入关系
 *		查找连接的输入关系。
 *
 * 我们假设输入关系的 RelOptInfo 必须已经构建完成。
 */
static RelOptInfo * fc_find_join_input_rel(PlannerInfo *fc_root, Relids fc_relids)
{
	RelOptInfo *fc_rel = NULL;

	switch (bms_membership(fc_relids))
	{
		case BMS_EMPTY_SET:
			/* 不应该发生 */
			break;
		case BMS_SINGLETON:
			fc_rel = find_base_rel(fc_root, bms_singleton_member(fc_relids));
			break;
		case BMS_MULTIPLE:
			fc_rel = find_join_rel(fc_root, fc_relids);
			break;
	}

	if (fc_rel == NULL)
		elog(ERROR, "could not find RelOptInfo for given relids");

	return fc_rel;
}


/*-------------------------------------------------------------------------
 *
 * 索引成本估算函数
 *
 *-------------------------------------------------------------------------
 */

/*
 * 从 IndexClause 列表中提取实际的 indexquals（作为 RestrictInfos）
 */
List * get_quals_from_indexclauses(List *fc_indexclauses)
{
	List	   *fc_result = NIL;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_indexclauses)
	{
		IndexClause *fc_iclause = lfirst_node(IndexClause, fc_lc);
		ListCell   *fc_lc2;

		foreach(fc_lc2, fc_iclause->indexquals)
		{
			RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_lc2);

			fc_result = lappend(fc_result, fc_rinfo);
		}
	}
	return fc_result;
}

/*
 * 计算一组索引条件表达式中比较操作数的总评估成本。由于我们知道这些将在每次扫描中仅评估一次，因此没有必要区分启动成本和逐行成本。
 *
 * 这可以用于 get_quals_from_indexclauses() 的结果，或直接用于 indexorderbys 列表。在这两种情况下，我们希望索引键表达式位于二元条件的左侧。
 */
Cost
index_other_operands_eval_cost(PlannerInfo *fc_root, List *fc_indexquals)
{
	Cost		fc_qual_arg_cost = 0;
	ListCell   *fc_lc;

	foreach(fc_lc, fc_indexquals)
	{
		Expr	   *fc_clause = (Expr *) lfirst(fc_lc);
		Node	   *fc_other_operand;
		QualCost	fc_index_qual_cost;

		/*
		 * 索引条件将包含 RestrictInfos，indexorderbys 则不会。如果存在，查看 RestrictInfo。
		 */
		if (IsA(fc_clause, RestrictInfo))
			fc_clause = ((RestrictInfo *) fc_clause)->clause;

		if (IsA(fc_clause, OpExpr))
		{
			OpExpr	   *fc_op = (OpExpr *) fc_clause;

			fc_other_operand = (Node *) lsecond(fc_op->args);
		}
		else if (IsA(fc_clause, RowCompareExpr))
		{
			RowCompareExpr *fc_rc = (RowCompareExpr *) fc_clause;

			fc_other_operand = (Node *) fc_rc->rargs;
		}
		else if (IsA(fc_clause, ScalarArrayOpExpr))
		{
			ScalarArrayOpExpr *fc_saop = (ScalarArrayOpExpr *) fc_clause;

			fc_other_operand = (Node *) lsecond(fc_saop->args);
		}
		else if (IsA(fc_clause, NullTest))
		{
			fc_other_operand = NULL;
		}
		else
		{
			elog(ERROR, "unsupported indexqual type: %d",
				 (int) nodeTag(fc_clause));
			fc_other_operand = NULL;	/* 保持编译器安静 */
		}

		cost_qual_eval_node(&fc_index_qual_cost, fc_other_operand, fc_root);
		fc_qual_arg_cost += fc_index_qual_cost.startup + fc_index_qual_cost.per_tuple;
	}
	return fc_qual_arg_cost;
}

void genericcostestimate(PlannerInfo *fc_root,
					IndexPath *fc_path,
					double fc_loop_count,
					GenericCosts *fc_costs)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	List	   *fc_indexQuals = get_quals_from_indexclauses(fc_path->indexclauses);
	List	   *fc_indexOrderBys = fc_path->indexorderbys;
	Cost		fc_indexStartupCost;
	Cost		fc_indexTotalCost;
	Selectivity fc_indexSelectivity;
	double		fc_indexCorrelation;
	double		fc_numIndexPages;
	double		fc_numIndexTuples;
	double		fc_spc_random_page_cost;
	double		fc_num_sa_scans;
	double		fc_num_outer_scans;
	double		fc_num_scans;
	double		fc_qual_op_cost;
	double		fc_qual_arg_cost;
	List	   *fc_selectivityQuals;
	ListCell   *fc_l;

	/*
	 * 如果索引是部分的，将索引谓词与显式给定的 indexquals 结合，以产生更准确的索引选择性概念。
	 */
	fc_selectivityQuals = add_predicate_to_index_quals(fc_index, fc_indexQuals);

	/*
	 * 检查 ScalarArrayOpExpr 索引条件，并估算将执行的索引扫描数量。
	 */
	fc_num_sa_scans = 1;
	foreach(fc_l, fc_indexQuals)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_l);

		if (IsA(fc_rinfo->clause, ScalarArrayOpExpr))
		{
			ScalarArrayOpExpr *fc_saop = (ScalarArrayOpExpr *) fc_rinfo->clause;
			int			fc_alength = estimate_array_length(lsecond(fc_saop->args));

			if (fc_alength > 1)
				fc_num_sa_scans *= fc_alength;
		}
	}

	/* 估算将访问的主表元组的比例 */
	fc_indexSelectivity = clauselist_selectivity(fc_root, fc_selectivityQuals,
											  fc_index->rel->relid,
											  JOIN_INNER,
											  NULL);

	/*
	 * 如果调用方没有给出估计值，则估计将访问的索引元组数量。我们以这种相对特殊的方式进行，以便对部分索引获取正确的答案。
	 */
	fc_numIndexTuples = fc_costs->numIndexTuples;
	if (fc_numIndexTuples <= 0.0)
	{
		fc_numIndexTuples = fc_indexSelectivity * fc_index->rel->tuples;

		/*
		 * 上面的计算统计了通过 ScalarArrayOpExpr 节点诱导的所有扫描中访问的所有元组。我们希望考虑每次索引扫描的平均数量，因此进行调整。这也是一个方便的地方，用于四舍五入到整数。（如果调用方提供了元组估计，它有责任处理这些考虑因素。）
		 */
		fc_numIndexTuples = rint(fc_numIndexTuples / fc_num_sa_scans);
	}

	/*
	 * 在任何情况下，我们都可以通过索引大小来限制元组的数量。此外，即使索引选择性估计很小，始终估算至少会触及一个元组。
	 */
	if (fc_numIndexTuples > fc_index->tuples)
		fc_numIndexTuples = fc_index->tuples;
	if (fc_numIndexTuples < 1.0)
		fc_numIndexTuples = 1.0;

	/*
	 * 估计将被检索的索引页数量。
	 *
	 * 我们使用简单的方法获取索引页总数的按比例分配。实际上，这只计算叶页，而不计算任何开销，例如索引元页面或上层树级。
	 *
	 * 实际上，访问上层索引级别通常几乎是免费的，因为这些在负载下往往停留在缓存中；此外，相关成本高度依赖于索引类型。因此，我们在这里忽略这些成本，并将其留给调用方在需要时添加适当的费用。
	 */
	if (fc_index->pages > 1 && fc_index->tuples > 1)
		fc_numIndexPages = ceil(fc_numIndexTuples * fc_index->pages / fc_index->tuples);
	else
		fc_numIndexPages = 1.0;

	/* 获取包含索引的表空间的估计页成本 */
	get_tablespace_page_costs(fc_index->reltablespace,
							  &fc_spc_random_page_cost,
							  NULL);

	/*
	 * 现在计算磁盘访问成本。
	 *
	 * 上面的计算都是针对每个索引扫描。然而，如果我们处于
	 * 嵌套循环内部扫描，我们可以预期扫描会为外部关系的每一行
	 * 进行重复（使用不同的搜索键）。同样，
	 * ScalarArrayOpExpr的条件会导致多个索引扫描。这样就会
	 * 有缓存效应降低所需的磁盘页面提取次数的潜力。我们希望
	 * 在存在缓存的情况下估算每次扫描的平均I/O成本。
	 *
	 * 我们使用Mackert-Lohman公式（详见costsize.c）来
	 * 估算发生的总页面提取次数。虽然这不是它设计的目的，
	 * 但在某种程度上似乎是一个合理的模型。
	 * 注意，我们不再计算元组，而是计算页面，因此我们
	 * 取N = T = 索引大小，就好像每个页面有一个“元组”一样。
	 */
	fc_num_outer_scans = fc_loop_count;
	fc_num_scans = fc_num_sa_scans * fc_num_outer_scans;

	if (fc_num_scans > 1)
	{
		double		fc_pages_fetched;

		/* 忽略缓存效应的总页面提取次数 */
		fc_pages_fetched = fc_numIndexPages * fc_num_scans;

		/* 使用Mackert和Lohman公式调整缓存效应 */
		fc_pages_fetched = index_pages_fetched(fc_pages_fetched,
											fc_index->pages,
											(double) fc_index->pages,
											fc_root);

		/*
		 * 现在计算总的磁盘访问成本，然后为每次外部扫描报告
		 * 一个比例分摊的部分。（对于ScalarArrayOpExpr，不要按比例分摊，
		 * 因为它是索引扫描内部的。）
		 */
		fc_indexTotalCost = (fc_pages_fetched * fc_spc_random_page_cost)
			/ fc_num_outer_scans;
	}
	else
	{
		/*
		 * 对于单个索引扫描，我们仅根据每个
		 * 被触及的页面收取spc_random_page_cost。
		 */
		fc_indexTotalCost = fc_numIndexPages * fc_spc_random_page_cost;
	}

	/*
	 * CPU成本：索引条件中的任何复杂表达式需要在扫描开始时
	 * 进行一次评估，以将其简化为运行时键以传递给索引AM（见
	 * nodeIndexscan.c）。我们将每个元组的
	 * CPU成本建模为cpu_index_tuple_cost加上每个
	 * indexqual运算符的一个cpu_operator_cost。
	 * 因为我们将numIndexTuples作为每次扫描的数量，
	 * 所以必须乘以num_sa_scans以获得ScalarArrayOpExpr
	 * 情况的正确结果。类似地，还要计算任何索引
	 * ORDER BY表达式的成本。
	 *
	 * 注意：这忽略了重新检查有损运算符可能带来的成本。
	 * 考虑到这里的所有其他不准确性，检测此类需求似乎
	 * 成本高于实际价值...
	 */
	fc_qual_arg_cost = index_other_operands_eval_cost(fc_root, fc_indexQuals) +
		index_other_operands_eval_cost(fc_root, fc_indexOrderBys);
	fc_qual_op_cost = cpu_operator_cost *
		(list_length(fc_indexQuals) + list_length(fc_indexOrderBys));

	fc_indexStartupCost = fc_qual_arg_cost;
	fc_indexTotalCost += fc_qual_arg_cost;
	fc_indexTotalCost += fc_numIndexTuples * fc_num_sa_scans * (cpu_index_tuple_cost + fc_qual_op_cost);

	/*
	 * 关于索引相关性的通用假设：没有相关性。
	 */
	fc_indexCorrelation = 0.0;

	/*
	 * 将所有内容返回给调用者。
	 */
	fc_costs->indexStartupCost = fc_indexStartupCost;
	fc_costs->indexTotalCost = fc_indexTotalCost;
	fc_costs->indexSelectivity = fc_indexSelectivity;
	fc_costs->indexCorrelation = fc_indexCorrelation;
	fc_costs->numIndexPages = fc_numIndexPages;
	fc_costs->numIndexTuples = fc_numIndexTuples;
	fc_costs->spc_random_page_cost = fc_spc_random_page_cost;
	fc_costs->num_sa_scans = fc_num_sa_scans;
}

/*
 * 如果索引是部分的，将其谓词添加到给定的条件列表中。
 *
 * 将索引谓词与显式给定的indexquals进行与操作可以产生
 * 更准确的索引选择性。然而，我们需要小心不要插入冗余
 * 子句，因为clauselist_selectivity()很容易被误导以计算
 * 一个过低的选择性估计。我们的方法是仅添加那些无法
 * 被证明由给定indexquals隐含的谓词子句。这成功处理
 * 了类似于“x = 42”的条件与部分索引“WHERE x >= 40 AND x < 50”
 * 一起使用的情况。还有许多其他情况我们不会检测到冗余，
 * 这将导致过低的选择性估计，这会使系统倾向于在可能的地方
 * 使用部分索引。不过，这并不一定是坏事。
 *
 * 注意，indexQuals包含RestrictInfo节点，而indpred
 * 则不包含，因此输出列表会混合。这对predicate_implied_by()
 * 和clauselist_selectivity()来说是可以的，但如果结果传递给
 * 其他内容可能会有问题。
 */
List * add_predicate_to_index_quals(IndexOptInfo *fc_index, List *fc_indexQuals)
{
	List	   *fc_predExtraQuals = NIL;
	ListCell   *fc_lc;

	if (fc_index->indpred == NIL)
		return fc_indexQuals;

	foreach(fc_lc, fc_index->indpred)
	{
		Node	   *fc_predQual = (Node *) lfirst(fc_lc);
		List	   *fc_oneQual = list_make1(fc_predQual);

		if (!predicate_implied_by(fc_oneQual, fc_indexQuals, false))
			fc_predExtraQuals = list_concat(fc_predExtraQuals, fc_oneQual);
	}
	return list_concat(fc_predExtraQuals, fc_indexQuals);
}


void btcostestimate(PlannerInfo *fc_root, IndexPath *fc_path, double fc_loop_count,
			   Cost *fc_indexStartupCost, Cost *fc_indexTotalCost,
			   Selectivity *fc_indexSelectivity, double *fc_indexCorrelation,
			   double *fc_indexPages)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	GenericCosts fc_costs;
	Oid			fc_relid;
	AttrNumber	fc_colnum;
	VariableStatData fc_vardata;
	double		fc_numIndexTuples;
	Cost		fc_descentCost;
	List	   *fc_indexBoundQuals;
	int			fc_indexcol;
	bool		fc_eqQualHere;
	bool		fc_found_saop;
	bool		fc_found_is_null_op;
	double		fc_num_sa_scans;
	ListCell   *fc_lc;

	/*
	 * 对于btree扫描，只有首屈一指的'='条件加上紧接着的
	 * 属性的不等式条件才能对索引选择性产生贡献
	 * （这些是决定索引扫描起止点的“边界条件”）。额外的条件
	 * 可以抑制对堆的访问，因此在indexSelectivity中计入它们是可以的，
	 * 但它们不应计入估算numIndexTuples。因此，我们必须检查
	 * 给定的indexquals，以找出哪些条件算作边界条件。我们依赖于
	 * 它们按索引列顺序给出的知识。
	 *
	 * 对于RowCompareExpr，我们只考虑第一个列，就像
	 * rowcomparesel()一样。
	 *
	 * 如果条件中有ScalarArrayOpExpr，我们实际上会执行N
	 * 个索引扫描，而不是一个，但ScalarArrayOpExpr的运算符
	 * 可以被认为是正常运行。
	 */
	fc_indexBoundQuals = NIL;
	fc_indexcol = 0;
	fc_eqQualHere = false;
	fc_found_saop = false;
	fc_found_is_null_op = false;
	fc_num_sa_scans = 1;
	foreach(fc_lc, fc_path->indexclauses)
	{
		IndexClause *fc_iclause = lfirst_node(IndexClause, fc_lc);
		ListCell   *fc_lc2;

		if (fc_indexcol != fc_iclause->indexcol)
		{
			/* 新列条件的开始 */
			if (!fc_eqQualHere)
				break;			/* 如果没有'='条件，则完成 */
			fc_eqQualHere = false;
			fc_indexcol++;
			if (fc_indexcol != fc_iclause->indexcol)
				break;			/* 对于indexcol没有任何条件 */
		}

		/* 检查与此索引子句相关联的每个indexqual */
		foreach(fc_lc2, fc_iclause->indexquals)
		{
			RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_lc2);
			Expr	   *fc_clause = fc_rinfo->clause;
			Oid			fc_clause_op = InvalidOid;
			int			fc_op_strategy;

			if (IsA(fc_clause, OpExpr))
			{
				OpExpr	   *fc_op = (OpExpr *) fc_clause;

				fc_clause_op = fc_op->opno;
			}
			else if (IsA(fc_clause, RowCompareExpr))
			{
				RowCompareExpr *fc_rc = (RowCompareExpr *) fc_clause;

				fc_clause_op = linitial_oid(fc_rc->opnos);
			}
			else if (IsA(fc_clause, ScalarArrayOpExpr))
			{
				ScalarArrayOpExpr *fc_saop = (ScalarArrayOpExpr *) fc_clause;
				Node	   *fc_other_operand = (Node *) lsecond(fc_saop->args);
				int			fc_alength = estimate_array_length(fc_other_operand);

				fc_clause_op = fc_saop->opno;
				fc_found_saop = true;
				/* 仅计算由indexBoundQuals引起的SA扫描次数 */
				if (fc_alength > 1)
					fc_num_sa_scans *= fc_alength;
			}
			else if (IsA(fc_clause, NullTest))
			{
				NullTest   *fc_nt = (NullTest *) fc_clause;

				if (fc_nt->nulltesttype == IS_NULL)
				{
					fc_found_is_null_op = true;
					/* IS NULL在选择性方面类似于'=' */
					fc_eqQualHere = true;
				}
			}
			else
				elog(ERROR, "unsupported indexqual type: %d",
					 (int) nodeTag(fc_clause));

			/* 检查等于运算符 */
			if (OidIsValid(fc_clause_op))
			{
				fc_op_strategy = get_op_opfamily_strategy(fc_clause_op,
													   fc_index->opfamily[fc_indexcol]);
				Assert(fc_op_strategy != 0);	/* 不是运算符家族的成员?? */
				if (fc_op_strategy == BTEqualStrategyNumber)
					fc_eqQualHere = true;
			}

			fc_indexBoundQuals = lappend(fc_indexBoundQuals, fc_rinfo);
		}
	}

	/*
	 * 如果索引是唯一的，并且我们为每一列找到了一个'='子句，
	 * 我们可以假设numIndexTuples = 1，并跳过昂贵的
	 * clauselist_selectivity计算。但是，ScalarArrayOp或
	 * NullTest会使该理论失效，即使它设置了eqQualHere。
	 */
	if (fc_index->unique &&
		fc_indexcol == fc_index->nkeycolumns - 1 &&
		fc_eqQualHere &&
		!fc_found_saop &&
		!fc_found_is_null_op)
		fc_numIndexTuples = 1.0;
	else
	{
		List	   *fc_selectivityQuals;
		Selectivity fc_btreeSelectivity;

		
/*
		 * 如果索引是部分的，并且使用索引谓词和索引限制条件来产生更准确的行数覆盖情况。
		 */
		fc_selectivityQuals = add_predicate_to_index_quals(fc_index, fc_indexBoundQuals);

		fc_btreeSelectivity = clauselist_selectivity(fc_root, fc_selectivityQuals,
												  fc_index->rel->relid,
												  JOIN_INNER,
												  NULL);
		fc_numIndexTuples = fc_btreeSelectivity * fc_index->rel->tuples;

		/*
		 * 像在genericcostestimate()中一样，我们必须调整包含在indexBoundQuals中的任何
		 * ScalarArrayOpExpr限值，然后四舍五入为整数。
		 */
		fc_numIndexTuples = rint(fc_numIndexTuples / fc_num_sa_scans);
	}

	/*
	 * 现在进行通用索引成本估算。
	 */
	MemSet(&fc_costs, 0, sizeof(fc_costs));
	fc_costs.numIndexTuples = fc_numIndexTuples;

	genericcostestimate(fc_root, fc_path, fc_loop_count, &fc_costs);

	/*
	 * 增加一个CPU成本组件来表示初始B树下降的成本。我们不对接触上层B树级别收取任何I/O成本，
	 * 因为它们往往保留在缓存中，但我们仍需执行大约log2(N)次比较以下降N叶元组的B树。
	 * 每次比较收取一个cpu_operator_cost。
	 *
	 * 如果有ScalarArrayOpExpr，每次SA扫描收取一次费用。至于总体计划而言，第一次之后的
	 * 费用并不是启动成本，因此仅将其添加到“总”成本中。
	 */
	if (fc_index->tuples > 1)		/* 避免计算log(0) */
	{
		fc_descentCost = ceil(log(fc_index->tuples) / log(2.0)) * cpu_operator_cost;
		fc_costs.indexStartupCost += fc_descentCost;
		fc_costs.indexTotalCost += fc_costs.num_sa_scans * fc_descentCost;
	}

	/*
	 * 尽管我们不对接触上层B树页面收取I/O成本，但每下降一页收取一些CPU成本仍然是合理的。
	 * 此外，如果完全没有这样的费用，那么膨胀的索引在搜索成本上的表现将与未膨胀的索引相同，
	 * 至少在仅期望访问单个叶页面的情况下。这项费用被相当随意地设定为每个接触页面50倍的
	 * cpu_operator_cost。这样的页面数量是B树的高度加一（即，我们也对叶页面收取费用）。
	 * 和上面一样，每次SA扫描收取一次费用。
	 */
	fc_descentCost = (fc_index->tree_height + 1) * 50.0 * cpu_operator_cost;
	fc_costs.indexStartupCost += fc_descentCost;
	fc_costs.indexTotalCost += fc_costs.num_sa_scans * fc_descentCost;

	/*
	 * 如果我们可以从pg_statistic获取第一列的排序相关性C，则将单列索引的索引相关性
	 * 估计为C，或多列索引估计为C * 0.75。（这里的想法是多个列使第一列排序的重要性稀释，
	 * 但并非完全消失。在8.0之前，我们将相关性除以列数，但这似乎太强了。）
	 */
	MemSet(&fc_vardata, 0, sizeof(fc_vardata));

	if (fc_index->indexkeys[0] != 0)
	{
		/* 简单变量——查看底层表的统计信息 */
		RangeTblEntry *fc_rte = planner_rt_fetch(fc_index->rel->relid, fc_root);

		Assert(fc_rte->rtekind == RTE_RELATION);
		fc_relid = fc_rte->relid;
		Assert(fc_relid != InvalidOid);
		fc_colnum = fc_index->indexkeys[0];

		if (get_relation_stats_hook &&
			(*get_relation_stats_hook) (fc_root, fc_rte, fc_colnum, &fc_vardata))
		{
			/*
			 * 钩子控制获取统计元组。如果它提供了元组，最好提供一个freefunc。
			 */
			if (HeapTupleIsValid(fc_vardata.statsTuple) &&
				!fc_vardata.freefunc)
				elog(ERROR, "no function provided to release variable stats with");
		}
		else
		{
			fc_vardata.statsTuple = SearchSysCache3(STATRELATTINH,
												 ObjectIdGetDatum(fc_relid),
												 Int16GetDatum(fc_colnum),
												 BoolGetDatum(fc_rte->inh));
			fc_vardata.freefunc = ReleaseSysCache;
		}
	}
	else
	{
		/* 表达式——也许有针对索引本身的统计信息 */
		fc_relid = fc_index->indexoid;
		fc_colnum = 1;

		if (get_index_stats_hook &&
			(*get_index_stats_hook) (fc_root, fc_relid, fc_colnum, &fc_vardata))
		{
			/*
			 * 钩子控制获取统计元组。如果它提供了元组，最好提供一个freefunc。
			 */
			if (HeapTupleIsValid(fc_vardata.statsTuple) &&
				!fc_vardata.freefunc)
				elog(ERROR, "no function provided to release variable stats with");
		}
		else
		{
			fc_vardata.statsTuple = SearchSysCache3(STATRELATTINH,
												 ObjectIdGetDatum(fc_relid),
												 Int16GetDatum(fc_colnum),
												 BoolGetDatum(false));
			fc_vardata.freefunc = ReleaseSysCache;
		}
	}

	if (HeapTupleIsValid(fc_vardata.statsTuple))
	{
		Oid			fc_sortop;
		AttStatsSlot fc_sslot;

		fc_sortop = get_opfamily_member(fc_index->opfamily[0],
									 fc_index->opcintype[0],
									 fc_index->opcintype[0],
									 BTLessStrategyNumber);
		if (OidIsValid(fc_sortop) &&
			get_attstatsslot(&fc_sslot, fc_vardata.statsTuple,
							 STATISTIC_KIND_CORRELATION, fc_sortop,
							 ATTSTATSSLOT_NUMBERS))
		{
			double		fc_varCorrelation;

			Assert(fc_sslot.nnumbers == 1);
			fc_varCorrelation = fc_sslot.numbers[0];

			if (fc_index->reverse_sort[0])
				fc_varCorrelation = -fc_varCorrelation;

			if (fc_index->nkeycolumns > 1)
				fc_costs.indexCorrelation = fc_varCorrelation * 0.75;
			else
				fc_costs.indexCorrelation = fc_varCorrelation;

			free_attstatsslot(&fc_sslot);
		}
	}

	ReleaseVariableStats(fc_vardata);

	*fc_indexStartupCost = fc_costs.indexStartupCost;
	*fc_indexTotalCost = fc_costs.indexTotalCost;
	*fc_indexSelectivity = fc_costs.indexSelectivity;
	*fc_indexCorrelation = fc_costs.indexCorrelation;
	*fc_indexPages = fc_costs.numIndexPages;
}

void hashcostestimate(PlannerInfo *fc_root, IndexPath *fc_path, double fc_loop_count,
				 Cost *fc_indexStartupCost, Cost *fc_indexTotalCost,
				 Selectivity *fc_indexSelectivity, double *fc_indexCorrelation,
				 double *fc_indexPages)
{
	GenericCosts fc_costs;

	MemSet(&fc_costs, 0, sizeof(fc_costs));

	genericcostestimate(fc_root, fc_path, fc_loop_count, &fc_costs);

	/*
	 * 哈希索引没有下降成本，因为索引AM可以在计算哈希值后直接进入目标桶。
	 * 但是我们可以在这里添加一些其他特定于哈希的成本：
	 *
	 * 理想情况下，我们应该对目标桶中每个页面收取spc_random_page_cost，而不仅仅是
	 * genericcostestimate认为我们将访问的numIndexPages页面。然而，在大多数情况下，
	 * 我们不知道那将是哪一个桶。考虑平均桶大小没有意义，因为哈希AM确保这始终是一个页面。
	 *
	 * 同样，如果我们知道桶中有多少个索引元组，我们可以考虑对每个索引元组收取一些CPU费用。
	 * 但是每个元组的成本只是一个哈希值比较，而不是一般数据类型相关的比较，因此任何此类费用
	 * 应该远低于cpu_operator_cost；这使得它可能不值得担心。
	 *
	 * 更大的问题是，偶然的哈希值冲突将导致在堆中浪费探测。我们目前不尝试对这一成本建模，
	 * 认为这种情况很少见，但可能并不够少。（任何对此的修复都应考虑通用的有损运算符问题；这并非
	 * 完全特定于哈希。）
	 */

	*fc_indexStartupCost = fc_costs.indexStartupCost;
	*fc_indexTotalCost = fc_costs.indexTotalCost;
	*fc_indexSelectivity = fc_costs.indexSelectivity;
	*fc_indexCorrelation = fc_costs.indexCorrelation;
	*fc_indexPages = fc_costs.numIndexPages;
}

void gistcostestimate(PlannerInfo *fc_root, IndexPath *fc_path, double fc_loop_count,
				 Cost *fc_indexStartupCost, Cost *fc_indexTotalCost,
				 Selectivity *fc_indexSelectivity, double *fc_indexCorrelation,
				 double *fc_indexPages)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	GenericCosts fc_costs;
	Cost		fc_descentCost;

	MemSet(&fc_costs, 0, sizeof(fc_costs));

	genericcostestimate(fc_root, fc_path, fc_loop_count, &fc_costs);

	/*
	 * 我们以类似于btree的方式来模拟索引下降的成本，但为了做到这一点，我们首先需要一个关于树高度的概念。我们有些任意地假设扇出为100，这意味着树的高度最多为log100(index->pages)。
	 *
	 * 尽管这个计算真正没有那么昂贵到需要缓存，但我们可以使用index->tree_height来缓存它。
	 */
	if (fc_index->tree_height < 0) /* unknown? */
	{
		if (fc_index->pages > 1)	/* 避免计算log(0) */
			fc_index->tree_height = (int) (log(fc_index->pages) / log(100.0));
		else
			fc_index->tree_height = 0;
	}

	/*
	 * 添加一个CPU成本组件来表示初始下降的成本。我们在这里只使用log(N)，而不是log2(N)，因为分支因子不一定是2。对于btree，每次SA扫描时收取一次费用。
	 */
	if (fc_index->tuples > 1)		/* 避免计算log(0) */
	{
		fc_descentCost = ceil(log(fc_index->tuples)) * cpu_operator_cost;
		fc_costs.indexStartupCost += fc_descentCost;
		fc_costs.indexTotalCost += fc_costs.num_sa_scans * fc_descentCost;
	}

	/*
	 * 同样地，添加一个每页费用，计算方式与btree相同。
	 */
	fc_descentCost = (fc_index->tree_height + 1) * 50.0 * cpu_operator_cost;
	fc_costs.indexStartupCost += fc_descentCost;
	fc_costs.indexTotalCost += fc_costs.num_sa_scans * fc_descentCost;

	*fc_indexStartupCost = fc_costs.indexStartupCost;
	*fc_indexTotalCost = fc_costs.indexTotalCost;
	*fc_indexSelectivity = fc_costs.indexSelectivity;
	*fc_indexCorrelation = fc_costs.indexCorrelation;
	*fc_indexPages = fc_costs.numIndexPages;
}

void spgcostestimate(PlannerInfo *fc_root, IndexPath *fc_path, double fc_loop_count,
				Cost *fc_indexStartupCost, Cost *fc_indexTotalCost,
				Selectivity *fc_indexSelectivity, double *fc_indexCorrelation,
				double *fc_indexPages)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	GenericCosts fc_costs;
	Cost		fc_descentCost;

	MemSet(&fc_costs, 0, sizeof(fc_costs));

	genericcostestimate(fc_root, fc_path, fc_loop_count, &fc_costs);

	/*
	 * 我们以类似于btree的方式来模拟索引下降的成本，但为了做到这一点，我们首先需要一个关于树高度的概念。我们有些任意地假设扇出为100，这意味着树的高度最多为log100(index->pages)。
	 *
	 * 尽管这个计算真正没有那么昂贵到需要缓存，但我们可以使用index->tree_height来缓存它。
	 */
	if (fc_index->tree_height < 0) /* unknown? */
	{
		if (fc_index->pages > 1)	/* 避免计算log(0) */
			fc_index->tree_height = (int) (log(fc_index->pages) / log(100.0));
		else
			fc_index->tree_height = 0;
	}

	/*
	 * 添加一个CPU成本组件来表示初始下降的成本。我们在这里只使用log(N)，而不是log2(N)，因为分支因子不一定是2。对于btree，每次SA扫描时收取一次费用。
	 */
	if (fc_index->tuples > 1)		/* 避免计算log(0) */
	{
		fc_descentCost = ceil(log(fc_index->tuples)) * cpu_operator_cost;
		fc_costs.indexStartupCost += fc_descentCost;
		fc_costs.indexTotalCost += fc_costs.num_sa_scans * fc_descentCost;
	}

	/*
	 * 同样地，添加一个每页费用，计算方式与btree相同。
	 */
	fc_descentCost = (fc_index->tree_height + 1) * 50.0 * cpu_operator_cost;
	fc_costs.indexStartupCost += fc_descentCost;
	fc_costs.indexTotalCost += fc_costs.num_sa_scans * fc_descentCost;

	*fc_indexStartupCost = fc_costs.indexStartupCost;
	*fc_indexTotalCost = fc_costs.indexTotalCost;
	*fc_indexSelectivity = fc_costs.indexSelectivity;
	*fc_indexCorrelation = fc_costs.indexCorrelation;
	*fc_indexPages = fc_costs.numIndexPages;
}


/*
 * gincostestimate的支持例程
 */

typedef struct
{
	bool		attHasFullScan[INDEX_MAX_KEYS];
	bool		attHasNormalScan[INDEX_MAX_KEYS];
	double		partialEntries;
	double		exactEntries;
	double		searchEntries;
	double		arrayScans;
} GinQualCounts;

/*
	 * 估计在测试给定的GIN查询时需要搜索的索引项数，并相应地增加*counts中的计数。如果查询是不可满足的，返回false。
	 */
static bool fc_gincost_pattern(IndexOptInfo *fc_index, int fc_indexcol,
				Oid fc_clause_op, Datum fc_query,
				GinQualCounts *fc_counts)
{
	FmgrInfo	fc_flinfo;
	Oid			fc_extractProcOid;
	Oid			fc_collation;
	int			fc_strategy_op;
	Oid			fc_lefttype,
				fc_righttype;
	int32		fc_nentries = 0;
	bool	   *fc_partial_matches = NULL;
	Pointer    *fc_extra_data = NULL;
	bool	   *fc_nullFlags = NULL;
	int32		fc_searchMode = GIN_SEARCH_MODE_DEFAULT;
	int32		fc_i;

	Assert(fc_indexcol < fc_index->nkeycolumns);

	/*
	 * 获取运算符的策略编号和索引opfamily中的声明输入数据类型。（我们不需要后者，但我们使用get_op_opfamily_properties，因为如果它找不到匹配的pg_amop条目会抛出错误。）
	 */
	get_op_opfamily_properties(fc_clause_op, fc_index->opfamily[fc_indexcol], false,
							   &fc_strategy_op, &fc_lefttype, &fc_righttype);

	/*
	 * GIN始终使用“默认”支持函数，即lefttype == righttype == opclass的opcintype（见relcache.c中的IndexSupportInitialize）。
	 */
	fc_extractProcOid = get_opfamily_proc(fc_index->opfamily[fc_indexcol],
									   fc_index->opcintype[fc_indexcol],
									   fc_index->opcintype[fc_indexcol],
									   GIN_EXTRACTQUERY_PROC);

	if (!OidIsValid(fc_extractProcOid))
	{
		/* 不应该发生；抛出与index_getprocinfo相同的错误 */
		elog(ERROR, "missing support function %d for attribute %d of index \"%s\"",
			 GIN_EXTRACTQUERY_PROC, fc_indexcol + 1,
			 get_rel_name(fc_index->indexoid));
	}

	/*
	 * 选择传递给extractProc的排序规则（应该与initGinState匹配）。
	 */
	if (OidIsValid(fc_index->indexcollations[fc_indexcol]))
		fc_collation = fc_index->indexcollations[fc_indexcol];
	else
		fc_collation = DEFAULT_COLLATION_OID;

	fmgr_info(fc_extractProcOid, &fc_flinfo);

	set_fn_opclass_options(&fc_flinfo, fc_index->opclassoptions[fc_indexcol]);

	FunctionCall7Coll(&fc_flinfo,
					  fc_collation,
					  fc_query,
					  PointerGetDatum(&fc_nentries),
					  UInt16GetDatum(fc_strategy_op),
					  PointerGetDatum(&fc_partial_matches),
					  PointerGetDatum(&fc_extra_data),
					  PointerGetDatum(&fc_nullFlags),
					  PointerGetDatum(&fc_searchMode));

	if (fc_nentries <= 0 && fc_searchMode == GIN_SEARCH_MODE_DEFAULT)
	{
		/* 不可能有匹配 */
		return false;
	}

	for (fc_i = 0; fc_i < fc_nentries; fc_i++)
	{
		/*
		 * 对于部分匹配，我们没有任何信息来估计索引中匹配条目的数量，因此，我们只是将其估计为100。
		 */
		if (fc_partial_matches && fc_partial_matches[fc_i])
			fc_counts->partialEntries += 100;
		else
			fc_counts->exactEntries++;

		fc_counts->searchEntries++;
	}

	if (fc_searchMode == GIN_SEARCH_MODE_DEFAULT)
	{
		fc_counts->attHasNormalScan[fc_indexcol] = true;
	}
	else if (fc_searchMode == GIN_SEARCH_MODE_INCLUDE_EMPTY)
	{
		/* 将“包含空值”视为精确匹配项 */
		fc_counts->attHasNormalScan[fc_indexcol] = true;
		fc_counts->exactEntries++;
		fc_counts->searchEntries++;
	}
	else
	{
		/* 这是GIN_SEARCH_MODE_ALL */
		fc_counts->attHasFullScan[fc_indexcol] = true;
	}

	return true;
}

/*
 * 估计在测试给定的GIN索引子句时需要搜索的索引项数，并相应地增加*counts中的计数。如果查询是不可满足的，返回false。
 */
static bool fc_gincost_opexpr(PlannerInfo *fc_root,
			   IndexOptInfo *fc_index,
			   int fc_indexcol,
			   OpExpr *fc_clause,
			   GinQualCounts *fc_counts)
{
	Oid			fc_clause_op = fc_clause->opno;
	Node	   *fc_operand = (Node *) lsecond(fc_clause->args);

	/* 积极地减少到常量，并通过重标记进行查找 */
	fc_operand = estimate_expression_value(fc_root, fc_operand);

	if (IsA(fc_operand, RelabelType))
		fc_operand = (Node *) ((RelabelType *) fc_operand)->arg;

	/*
	 * 对于未知操作数，不可能调用extractQuery方法。因此，除非操作数是Const，我们就无法做太多；只需假设在运行时将有一个来自操作数的普通搜索条目。
	 */
	if (!IsA(fc_operand, Const))
	{
		fc_counts->exactEntries++;
		fc_counts->searchEntries++;
		return true;
	}

	/* 如果Const为null，则不可能有匹配 */
	if (((Const *) fc_operand)->constisnull)
		return false;

	/* 否则，应用extractQuery并获取实际的项计数 */
	return fc_gincost_pattern(fc_index, fc_indexcol, fc_clause_op,
						   ((Const *) fc_operand)->constvalue,
						   fc_counts);
}

/*
 * 估计在测试给定的GIN索引子句时需要搜索的索引项数，并相应地增加*counts中的计数。如果查询是不可满足的，返回false。
 *
 * 一个ScalarArrayOpExpr将在运行时产生N个单独的索引扫描，每个扫描涉及来自RHS数组的一个值，以及所有非数组资格（如果有）。为了建模这一点，我们在RHS元素中平均计数，并将平均值添加到*counts中的计数（对应每个索引扫描的成本）。我们还将counts->arrayScans乘以N，从而导致gincostestimate相应地放大其估计。
 */
static bool fc_gincost_scalararrayopexpr(PlannerInfo *fc_root,
						  IndexOptInfo *fc_index,
						  int fc_indexcol,
						  ScalarArrayOpExpr *fc_clause,
						  double fc_numIndexEntries,
						  GinQualCounts *fc_counts)
{
	Oid			fc_clause_op = fc_clause->opno;
	Node	   *fc_rightop = (Node *) lsecond(fc_clause->args);
	ArrayType  *fc_arrayval;
	int16		fc_elmlen;
	bool		fc_elmbyval;
	char		fc_elmalign;
	int			fc_numElems;
	Datum	   *fc_elemValues;
	bool	   *fc_elemNulls;
	GinQualCounts fc_arraycounts;
	int			fc_numPossible = 0;
	int			fc_i;

	Assert(fc_clause->useOr);

	/* 积极地减少到常量，并通过重标记进行查找 */
	fc_rightop = estimate_expression_value(fc_root, fc_rightop);

	if (IsA(fc_rightop, RelabelType))
		fc_rightop = (Node *) ((RelabelType *) fc_rightop)->arg;

	/*
	 * 对于未知操作数，不可能调用extractQuery方法。因此，除非操作数是Const，我们无法做太多；只需假设在运行时每个数组条目都会有一个普通搜索条目，并依赖于数组条目数量的可能不佳估计。
	 */
	if (!IsA(fc_rightop, Const))
	{
		fc_counts->exactEntries++;
		fc_counts->searchEntries++;
		fc_counts->arrayScans *= estimate_array_length(fc_rightop);
		return true;
	}

	/* 如果Const为null，则不可能有匹配 */
	if (((Const *) fc_rightop)->constisnull)
		return false;

	/* 否则，提取数组元素并对其进行迭代 */
	fc_arrayval = DatumGetArrayTypeP(((Const *) fc_rightop)->constvalue);
	get_typlenbyvalalign(ARR_ELEMTYPE(fc_arrayval),
						 &fc_elmlen, &fc_elmbyval, &fc_elmalign);
	deconstruct_array(fc_arrayval,
					  ARR_ELEMTYPE(fc_arrayval),
					  fc_elmlen, fc_elmbyval, fc_elmalign,
					  &fc_elemValues, &fc_elemNulls, &fc_numElems);

	memset(&fc_arraycounts, 0, sizeof(fc_arraycounts));

	for (fc_i = 0; fc_i < fc_numElems; fc_i++)
	{
		GinQualCounts fc_elemcounts;

		/* NULL不能匹配任何东西，因此忽略，因为执行器会这样做 */
		if (fc_elemNulls[fc_i])
			continue;

		/* 否则，应用extractQuery并获取实际的项计数 */
		memset(&fc_elemcounts, 0, sizeof(fc_elemcounts));

		if (fc_gincost_pattern(fc_index, fc_indexcol, fc_clause_op, fc_elemValues[fc_i],
							&fc_elemcounts))
		{
			/* 我们忽略不满足的模式的数组元素 */
			fc_numPossible++;

			if (fc_elemcounts.attHasFullScan[fc_indexcol] &&
				!fc_elemcounts.attHasNormalScan[fc_indexcol])
			{
				/*
				 * 将需要进行完全索引扫描。我们将其视为查询中列出了索引中的每个键；这样合理吗？
				 */
				fc_elemcounts.partialEntries = 0;
				fc_elemcounts.exactEntries = fc_numIndexEntries;
				fc_elemcounts.searchEntries = fc_numIndexEntries;
			}
			fc_arraycounts.partialEntries += fc_elemcounts.partialEntries;
			fc_arraycounts.exactEntries += fc_elemcounts.exactEntries;
			fc_arraycounts.searchEntries += fc_elemcounts.searchEntries;
		}
	}

	if (fc_numPossible == 0)
	{
		/* 数组中没有可满足的模式 */
		return false;
	}

	/*
	 * 现在将平均值添加到全局计数中。这将给我们一个估计，在每个索引扫描中搜索的项的平均数量，包括来自数组和非数组资格的贡献。
	 */
	fc_counts->partialEntries += fc_arraycounts.partialEntries / fc_numPossible;
	fc_counts->exactEntries += fc_arraycounts.exactEntries / fc_numPossible;
	fc_counts->searchEntries += fc_arraycounts.searchEntries / fc_numPossible;

	fc_counts->arrayScans *= fc_numPossible;

	return true;
}

/*
 * GIN的搜索行为与其他索引类型完全不同
 */
void gincostestimate(PlannerInfo *fc_root, IndexPath *fc_path, double fc_loop_count,
				Cost *fc_indexStartupCost, Cost *fc_indexTotalCost,
				Selectivity *fc_indexSelectivity, double *fc_indexCorrelation,
				double *fc_indexPages)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	List	   *fc_indexQuals = get_quals_from_indexclauses(fc_path->indexclauses);
	List	   *fc_selectivityQuals;
	double		fc_numPages = fc_index->pages,
				fc_numTuples = fc_index->tuples;
	double		fc_numEntryPages,
				fc_numDataPages,
				fc_numPendingPages,
				fc_numEntries;
	GinQualCounts fc_counts;
	bool		fc_matchPossible;
	bool		fc_fullIndexScan;
	double		fc_partialScale;
	double		fc_entryPagesFetched,
				fc_dataPagesFetched,
				fc_dataPagesFetchedBySel;
	double		fc_qual_op_cost,
				fc_qual_arg_cost,
				fc_spc_random_page_cost,
				fc_outer_scans;
	Relation	fc_indexRel;
	GinStatsData fc_ginStats;
	ListCell   *fc_lc;
	int			fc_i;

	/*
	 * 如果可能，从元页面获取统计信息。否则，将ginStats设置为零，我们随后将处理。
	 */
	if (!fc_index->hypothetical)
	{
		/* 锁应已在plancat.c中获取 */
		fc_indexRel = index_open(fc_index->indexoid, NoLock);
		ginGetStats(fc_indexRel, &fc_ginStats);
		index_close(fc_indexRel, NoLock);
	}
	else
	{
		memset(&fc_ginStats, 0, sizeof(fc_ginStats));
	}

	
	if (fc_ginStats.nPendingPages < fc_numPages)
		fc_numPendingPages = fc_ginStats.nPendingPages;
	else
		fc_numPendingPages = 0;

	if (fc_numPages > 0 && fc_ginStats.nTotalPages <= fc_numPages &&
		fc_ginStats.nTotalPages > fc_numPages / 4 &&
		fc_ginStats.nEntryPages > 0 && fc_ginStats.nEntries > 0)
	{
		/*
		 * 好吧，这些统计数据似乎足够接近理智，可以被信任。但是我们
		 * 仍然需要按比例numPages / nTotalPages进行缩放，以
		 * 考虑自上次VACUUM以来的增长。
		 */
		double		fc_scale = fc_numPages / fc_ginStats.nTotalPages;

		fc_numEntryPages = ceil(fc_ginStats.nEntryPages * fc_scale);
		fc_numDataPages = ceil(fc_ginStats.nDataPages * fc_scale);
		fc_numEntries = ceil(fc_ginStats.nEntries * fc_scale);
		/* 确保我们没有四舍五入太多 */
		fc_numEntryPages = Min(fc_numEntryPages, fc_numPages - fc_numPendingPages);
		fc_numDataPages = Min(fc_numDataPages,
						   fc_numPages - fc_numPendingPages - fc_numEntryPages);
	}
	else
	{
		/*
		 * 我们来到这里可能是因为这是一个假设的索引，或者是一个在
		 * 升级后从未进行过VACUUM的9.1之前创建的索引（在这种情况下
		 * 它的统计数据将显示为零），或者只是因为自上次VACUUM以来
		 * 它的增长太多而无法让我们相信缩放。
		 *
		 * 根据索引页数发明一些合理的内部统计数据（并将其限制为
		 * 至少10页，以防万一）。我们估计索引中90%的页面是条目页，
		 * 其余的是数据页。估计每个条目页有100个条目；这相当于虚假
		 * 数据，因为它取决于键的大小，但这比试图预测每个堆元组
		 * 的条目数量更稳健。
		 */
		fc_numPages = Max(fc_numPages, 10);
		fc_numEntryPages = floor((fc_numPages - fc_numPendingPages) * 0.90);
		fc_numDataPages = fc_numPages - fc_numPendingPages - fc_numEntryPages;
		fc_numEntries = floor(fc_numEntryPages * 100);
	}

	/* 在空索引中，numEntries可能为零。避免除以零 */
	if (fc_numEntries < 1)
		fc_numEntries = 1;

	/*
	 * 如果索引是部分的，并且索引谓词与索引约束条件结合以
	 * 产生更准确的受约束条件覆盖的行数的想法。
	 */
	fc_selectivityQuals = add_predicate_to_index_quals(fc_index, fc_indexQuals);

	/* 估算将访问的主表元组的比例 */
	*fc_indexSelectivity = clauselist_selectivity(fc_root, fc_selectivityQuals,
											   fc_index->rel->relid,
											   JOIN_INNER,
											   NULL);

	/* 获取包含索引的表空间的估计页成本 */
	get_tablespace_page_costs(fc_index->reltablespace,
							  &fc_spc_random_page_cost,
							  NULL);

	/*
	 * 关于索引相关性的通用假设：没有相关性。
	 */
	*fc_indexCorrelation = 0.0;

	/*
	 * 检查约束条件以估计搜索条目和部分匹配的数量
	 */
	memset(&fc_counts, 0, sizeof(fc_counts));
	fc_counts.arrayScans = 1;
	fc_matchPossible = true;

	foreach(fc_lc, fc_path->indexclauses)
	{
		IndexClause *fc_iclause = lfirst_node(IndexClause, fc_lc);
		ListCell   *fc_lc2;

		foreach(fc_lc2, fc_iclause->indexquals)
		{
			RestrictInfo *fc_rinfo = lfirst_node(RestrictInfo, fc_lc2);
			Expr	   *fc_clause = fc_rinfo->clause;

			if (IsA(fc_clause, OpExpr))
			{
				fc_matchPossible = fc_gincost_opexpr(fc_root,
											   fc_index,
											   fc_iclause->indexcol,
											   (OpExpr *) fc_clause,
											   &fc_counts);
				if (!fc_matchPossible)
					break;
			}
			else if (IsA(fc_clause, ScalarArrayOpExpr))
			{
				fc_matchPossible = fc_gincost_scalararrayopexpr(fc_root,
														  fc_index,
														  fc_iclause->indexcol,
														  (ScalarArrayOpExpr *) fc_clause,
														  fc_numEntries,
														  &fc_counts);
				if (!fc_matchPossible)
					break;
			}
			else
			{
				/* GIN索引不应该有其他内容 */
				elog(ERROR, "unsupported GIN indexqual type: %d",
					 (int) nodeTag(fc_clause));
			}
		}
	}

	/* 如果存在任何证明不可满足的约束条件，则退出 */
	if (!fc_matchPossible)
	{
		*fc_indexStartupCost = 0;
		*fc_indexTotalCost = 0;
		*fc_indexSelectivity = 0;
		return;
	}

	/*
	 * 如果属性有完全扫描，同时没有正常扫描，则我们必须扫描
	 * 该属性的所有非空条目。目前，我们没有GIN的每个属性统计。
	 * 因此，在这种情况下，我们必须假设整个GIN索引都必须被
	 * 扫描。
	 */
	fc_fullIndexScan = false;
	for (fc_i = 0; fc_i < fc_index->nkeycolumns; fc_i++)
	{
		if (fc_counts.attHasFullScan[fc_i] && !fc_counts.attHasNormalScan[fc_i])
		{
			fc_fullIndexScan = true;
			break;
		}
	}

	if (fc_fullIndexScan || fc_indexQuals == NIL)
	{
		/*
		 * 需要进行完全索引扫描。我们把这视为查询中列出了索引
		 * 中的每个键；这样合理吗？
		 */
		fc_counts.partialEntries = 0;
		fc_counts.exactEntries = fc_numEntries;
		fc_counts.searchEntries = fc_numEntries;
	}

	/* 我们会有超过一次嵌套循环扫描吗？ */
	fc_outer_scans = fc_loop_count;

	/*
	 * 计算开始扫描的成本，首先关注待处理列表。
	 */
	fc_entryPagesFetched = fc_numPendingPages;

	/*
	 * 估计读取的条目页数。我们需要执行counts.searchEntries次搜索。
	 * 使用一个幂函数，但叶页上的元组通常要大得多。在这里，我们
	 * 包括条目树中的所有搜索，包括部分匹配算法中第一个条目的
	 * 搜索。
	 */
	fc_entryPagesFetched += ceil(fc_counts.searchEntries * rint(pow(fc_numEntryPages, 0.15)));

	/*
	 * 添加部分匹配算法读取的条目页的估计。这是对条目树中
	 * 叶页的扫描。我们这里没有任何有用的统计数据，因此将其
	 * 估计为比例。因为counts.partialEntries确实相当虚假（见
	 * 上述代码），这个比例有可能超过numEntries；将比例限制
	 * 以确保合理性。
	 */
	fc_partialScale = fc_counts.partialEntries / fc_numEntries;
	fc_partialScale = Min(fc_partialScale, 1.0);

	fc_entryPagesFetched += ceil(fc_numEntryPages * fc_partialScale);

	/*
	 * 部分匹配算法在进行实际扫描之前读取所有数据页，
	 * 所以这是一个启动成本。同样，我们这里没有任何有用的
	 * 统计数据，因此将其估计为比例。
	 */
	fc_dataPagesFetched = ceil(fc_numDataPages * fc_partialScale);

	/*
	 * 如果由于嵌套循环或数组约束而需要进行多次扫描，计算缓存影响。
	 * 结果按嵌套循环扫描比例分配，但数组约束因素不应该按比例分配
	 * （比较genericcostestimate）。
	 */
	if (fc_outer_scans > 1 || fc_counts.arrayScans > 1)
	{
		fc_entryPagesFetched *= fc_outer_scans * fc_counts.arrayScans;
		fc_entryPagesFetched = index_pages_fetched(fc_entryPagesFetched,
												(BlockNumber) fc_numEntryPages,
												fc_numEntryPages, fc_root);
		fc_entryPagesFetched /= fc_outer_scans;
		fc_dataPagesFetched *= fc_outer_scans * fc_counts.arrayScans;
		fc_dataPagesFetched = index_pages_fetched(fc_dataPagesFetched,
											   (BlockNumber) fc_numDataPages,
											   fc_numDataPages, fc_root);
		fc_dataPagesFetched /= fc_outer_scans;
	}

	/*
	 * 在这里我们使用随机页面成本，因为逻辑上相邻的页面在
	 * 磁盘上可能相距甚远。
	 */
	*fc_indexStartupCost = (fc_entryPagesFetched + fc_dataPagesFetched) * fc_spc_random_page_cost;

	/*
	 * 现在计算扫描过程中获取的数据页数量。
	 *
	 * 我们假设每个条目有相同数量的项，而且它们之间没有重叠。
	 * （XXX：tsvector和array操作类收集单个键频率的统计信息；
	 * 如果能在这里使用这些数据就好了。）
	 */
	fc_dataPagesFetched = ceil(fc_numDataPages * fc_counts.exactEntries / fc_numEntries);

	/*
	 * 如果条目之间有很多重叠，特别是如果其中一个条目非常频繁，
	 * 上述计算可能会严重低估。作为简单的交叉检查，基于
	 * 约束条件的整体选择性计算下限。至少，我们必须为每个匹配
	 * 条目读取一个项指针。
	 *
	 * 每个项指针的宽度各不相同，取决于压缩级别。我们没有关于
	 * 此的统计数据，但每项大约3字节的平均值是相当典型的。
	 */
	fc_dataPagesFetchedBySel = ceil(*fc_indexSelectivity *
								 (fc_numTuples / (BLCKSZ / 3)));
	if (fc_dataPagesFetchedBySel > fc_dataPagesFetched)
		fc_dataPagesFetched = fc_dataPagesFetchedBySel;

	/* 考虑缓存影响，同上 */
	if (fc_outer_scans > 1 || fc_counts.arrayScans > 1)
	{
		fc_dataPagesFetched *= fc_outer_scans * fc_counts.arrayScans;
		fc_dataPagesFetched = index_pages_fetched(fc_dataPagesFetched,
											   (BlockNumber) fc_numDataPages,
											   fc_numDataPages, fc_root);
		fc_dataPagesFetched /= fc_outer_scans;
	}

	/* 并将random_page_cost应用为每页的成本 */
	*fc_indexTotalCost = *fc_indexStartupCost +
		fc_dataPagesFetched * fc_spc_random_page_cost;

	/*
	 * 加上索引谓词评估成本，类似于genericcostestimate。但我们
	 * 可以忽略indexorderby，因为GIN不支持这些。
	 */
	fc_qual_arg_cost = index_other_operands_eval_cost(fc_root, fc_indexQuals);
	fc_qual_op_cost = cpu_operator_cost * list_length(fc_indexQuals);

	*fc_indexStartupCost += fc_qual_arg_cost;
	*fc_indexTotalCost += fc_qual_arg_cost;
	*fc_indexTotalCost += (fc_numTuples * *fc_indexSelectivity) * (cpu_index_tuple_cost + fc_qual_op_cost);
	*fc_indexPages = fc_dataPagesFetched;
}

/*
 * BRIN的搜索行为与其他索引类型完全不同
 */
void brincostestimate(PlannerInfo *fc_root, IndexPath *fc_path, double fc_loop_count,
				 Cost *fc_indexStartupCost, Cost *fc_indexTotalCost,
				 Selectivity *fc_indexSelectivity, double *fc_indexCorrelation,
				 double *fc_indexPages)
{
	IndexOptInfo *fc_index = fc_path->indexinfo;
	List	   *fc_indexQuals = get_quals_from_indexclauses(fc_path->indexclauses);
	double		fc_numPages = fc_index->pages;
	RelOptInfo *fc_baserel = fc_index->rel;
	RangeTblEntry *fc_rte = planner_rt_fetch(fc_baserel->relid, fc_root);
	Cost		fc_spc_seq_page_cost;
	Cost		fc_spc_random_page_cost;
	double		fc_qual_arg_cost;
	double		fc_qualSelectivity;
	BrinStatsData fc_statsData;
	double		fc_indexRanges;
	double		fc_minimalRanges;
	double		fc_estimatedRanges;
	double		fc_selec;
	Relation	fc_indexRel;
	ListCell   *fc_l;
	VariableStatData fc_vardata;

	Assert(fc_rte->rtekind == RTE_RELATION);

	/* 获取包含索引的表空间的估计页面成本 */
	get_tablespace_page_costs(fc_index->reltablespace,
							  &fc_spc_random_page_cost,
							  &fc_spc_seq_page_cost);

	/*
	 * 如果可能，从索引本身获取一些数据。否则，根据关系页数
	 * 发明一些合理的内部统计数据。
	 */
	if (!fc_index->hypothetical)
	{
		/*
		 * 应该已经在plancat.c中获得了对索引的锁定。
		 */
		fc_indexRel = index_open(fc_index->indexoid, NoLock);
		brinGetStats(fc_indexRel, &fc_statsData);
		index_close(fc_indexRel, NoLock);

		/* 计算索引中实际范围的数量 */
		fc_indexRanges = Max(ceil((double) fc_baserel->pages /
							   fc_statsData.pagesPerRange), 1.0);
	}
	else
	{
		
/*
		 * 假设每个范围的默认页数，并根据此估计范围的数量。
		 */
		fc_indexRanges = Max(ceil((double) fc_baserel->pages /
							   BRIN_DEFAULT_PAGES_PER_RANGE), 1.0);

		fc_statsData.pagesPerRange = BRIN_DEFAULT_PAGES_PER_RANGE;
		fc_statsData.revmapNumPages = (fc_indexRanges / REVMAP_PAGE_MAXITEMS) + 1;
	}

	/*
	 * 计算索引关联
	 *
	 * 因为在扫描时我们可以平等地使用所有索引条件，所以我们可以使用
	 * 查询中使用的列中的最大关联（绝对值）。从零开始，这是最糟糕的情况。如果找不到任何
	 * 关联统计信息，我们将保持为0。
	 */
	*fc_indexCorrelation = 0;

	foreach(fc_l, fc_path->indexclauses)
	{
		IndexClause *fc_iclause = lfirst_node(IndexClause, fc_l);
		AttrNumber	fc_attnum = fc_index->indexkeys[fc_iclause->indexcol];

		/* 尝试查找此索引列在关系中的统计信息 */
		if (fc_attnum != 0)
		{
			/* 简单变量 - 查看基本表的统计信息 */
			if (get_relation_stats_hook &&
				(*get_relation_stats_hook) (fc_root, fc_rte, fc_attnum, &fc_vardata))
			{
				/*
				 * 钩子控制了获取统计元组的过程。如果它提供了一个元组，最好提供一个 freefunc。
				 */
				if (HeapTupleIsValid(fc_vardata.statsTuple) && !fc_vardata.freefunc)
					elog(ERROR,
						 "no function provided to release variable stats with");
			}
			else
			{
				fc_vardata.statsTuple =
					SearchSysCache3(STATRELATTINH,
									ObjectIdGetDatum(fc_rte->relid),
									Int16GetDatum(fc_attnum),
									BoolGetDatum(false));
				fc_vardata.freefunc = ReleaseSysCache;
			}
		}
		else
		{
			/*
			 * 看起来我们在索引中找到了一个表达式列。让我们看看是否有任何统计信息。
			 */

			/* 从基于 0 的索引中获取 attnum。 */
			fc_attnum = fc_iclause->indexcol + 1;

			if (get_index_stats_hook &&
				(*get_index_stats_hook) (fc_root, fc_index->indexoid, fc_attnum, &fc_vardata))
			{
				/*
				 * 钩子控制了获取统计元组的过程。如果它提供了一个元组，最好提供一个 freefunc。
				 */
				if (HeapTupleIsValid(fc_vardata.statsTuple) &&
					!fc_vardata.freefunc)
					elog(ERROR, "no function provided to release variable stats with");
			}
			else
			{
				fc_vardata.statsTuple = SearchSysCache3(STATRELATTINH,
													 ObjectIdGetDatum(fc_index->indexoid),
													 Int16GetDatum(fc_attnum),
													 BoolGetDatum(false));
				fc_vardata.freefunc = ReleaseSysCache;
			}
		}

		if (HeapTupleIsValid(fc_vardata.statsTuple))
		{
			AttStatsSlot fc_sslot;

			if (get_attstatsslot(&fc_sslot, fc_vardata.statsTuple,
								 STATISTIC_KIND_CORRELATION, InvalidOid,
								 ATTSTATSSLOT_NUMBERS))
			{
				double		fc_varCorrelation = 0.0;

				if (fc_sslot.nnumbers > 0)
					fc_varCorrelation = Abs(fc_sslot.numbers[0]);

				if (fc_varCorrelation > *fc_indexCorrelation)
					*fc_indexCorrelation = fc_varCorrelation;

				free_attstatsslot(&fc_sslot);
			}
		}

		ReleaseVariableStats(fc_vardata);
	}

	fc_qualSelectivity = clauselist_selectivity(fc_root, fc_indexQuals,
											 fc_baserel->relid,
											 JOIN_INNER, NULL);

	/*
	 * 现在计算如果表的堆中的所有行按完美顺序排列，我们能够匹配的最小可能范围。
	 */
	fc_minimalRanges = ceil(fc_indexRanges * fc_qualSelectivity);

	/*
	 * 现在估计我们将接触到的范围数量，使用来自统计信息的 indexCorrelation。小心不要除以零（注意我们使用的是相关系数的绝对值）。
	 */
	if (*fc_indexCorrelation < 1.0e-10)
		fc_estimatedRanges = fc_indexRanges;
	else
		fc_estimatedRanges = Min(fc_minimalRanges / *fc_indexCorrelation, fc_indexRanges);

	/* 我们预计将访问表的这一部分 */
	fc_selec = fc_estimatedRanges / fc_indexRanges;

	CLAMP_PROBABILITY(fc_selec);

	*fc_indexSelectivity = fc_selec;

	/*
	 * 计算索引条件成本，类似于 genericcostestimate，以添加到索引成本中。我们可以忽略 indexorderbys，因为 BRIN 不支持这些。
	 */
	fc_qual_arg_cost = index_other_operands_eval_cost(fc_root, fc_indexQuals);

	/*
	 * 计算启动成本，作为顺序读取整个 revmap 的成本，包括执行索引条件的成本。
	 */
	*fc_indexStartupCost =
		fc_spc_seq_page_cost * fc_statsData.revmapNumPages * fc_loop_count;
	*fc_indexStartupCost += fc_qual_arg_cost;

	/*
	 * 要读取一个 BRIN 索引，可能会在常规页面之间有一些来回，因为 revmap 可能会指向它们的顺序不是顺序的；计算总成本为以随机顺序读取整个索引。
	 */
	*fc_indexTotalCost = *fc_indexStartupCost +
		fc_spc_random_page_cost * (fc_numPages - fc_statsData.revmapNumPages) * fc_loop_count;

	/*
	 * 对于我们预计会匹配的每个范围元组收取少量费用。这是为了反映操作位图的成本。当我们找到匹配范围时，BRIN 扫描将为范围内的每个页面设置一个位，因此我们必须将费用乘以范围内的页面数量。
	 */
	*fc_indexTotalCost += 0.1 * cpu_operator_cost * fc_estimatedRanges *
		fc_statsData.pagesPerRange;

	*fc_indexPages = fc_index->pages;
}
